diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..25fe464 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,28 @@ +# Normalize line endings to avoid CRLF/LF churn bloating diffs. +# +# `text=auto` tells git to store text files with LF in the repository while +# letting each checkout use the platform's native endings (Windows users +# still get CRLF locally via core.autocrlf). This keeps blob line endings +# consistent regardless of contributor OS or editor, so a file's EOL no +# longer flips and inflates a diff. +* text=auto + +# Explicit source-type declarations (all normalized to LF in the repo). +*.py text +*.md text +*.txt text +*.csv text +*.ini text +*.cfg text +*.toml text +*.yml text +*.json text +*.log text +*.A text +*.sh text eol=lf + +# Binary assets — never apply end-of-line conversion to these. +*.stl binary +*.STL binary +*.png binary +*.pw binary diff --git a/.gitignore b/.gitignore index 875bfb3..1201f39 100755 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ -__pycache__/ -img/ -venv-pyrpod/ - -data/stl/groups/ -data/stl/tcd/ - -results/ - -results.txt -*.pyc -*.png -*.vtu -*.log +__pycache__/ +img/ +venv-pyrpod/ + +data/stl/groups/ +data/stl/tcd/ + +results/ + +results.txt +*.pyc +*.png +*.vtu +*.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cac98c1..141bd7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,70 +1,70 @@ -# Contributing to PyRPOD - -Please read carefully before contributing to PyRPOD. - -## Code Contributions - -To contribute to the PyRPOD source code, you must agree to the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). - -By submitting a pull request, you implicitly agree to the certifications and terms of the DCO. No additional actions are required. - -We aim to make our contribution process as developer-friendly as possible while maintaining high-quality standards. - -## Bug Reports - -If you identify a bug, please open an issue in the repository. Provide detailed information, including: - -- Steps to reproduce the issue. -- Expected versus actual behavior. -- Any relevant code snippets or configurations. - -Reports should ideally include minimal code examples and associated outputs to make troubleshooting easier. - -## Feature Requests - -Feature requests can be submitted as issues. While we strive to accommodate requests, keep in mind that PyRPOD is an open-source project with limited resources. We may provide guidance on implementing your requested features. - -## Contributions of Novel Features - -PyRPOD accepts contributions from community members with demonstrated programming skills and familiarity with the PyRPOD framework. Before starting significant development efforts: - -1. Review the PyRPOD documentation and style guides. -2. Engage with maintainers and the community to validate your proposal's alignment with PyRPOD's goals. - -To contribute: - -1. Fork the repository following the [forking workflow guide](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/working-with-forks). -2. Create new modules, functions, or classes instead of altering existing ones wherever possible. -3. Document your additions thoroughly, including: - - Code comments. - - References to academic papers if applicable. - - Example usage or test cases. - -When your contribution is ready, submit a pull request. Include a clear summary of your changes, their purpose, and any critical details. - -## Testing and Continuous Integration (CI) - -PyRPOD uses [pytest](https://docs.pytest.org/) to ensure stability and prevent regressions. To facilitate this: - -1. Write tests for your contributions in the `tests` directory, following the existing `__test_NN.py` naming convention (e.g. `rpod_unit_test_04.py`). -2. Run the full suite locally with `pytest` from the repository root before submitting a pull request. -3. Tests are automatically tagged with `unit`, `integration`, `verification`, and subsystem (`mdao`, `mission`, `plume`, `rpod`) markers based on their filename, so you can run a subset with e.g. `pytest -m unit` or `pytest -m rpod`. -4. CI runs the same `pytest` suite automatically on every push and pull request via [GitHub Actions](.github/workflows/tests.yml). - -## Code Formatting - -Consistency is key. PyRPOD enforces uniform code formatting using the `black` code formatter. - -- Install and run `black` on your code before committing: - ```bash - black . - ``` -- The CI pipeline will reject pull requests with improperly formatted code. - -## Licensing - -By contributing, you agree that your contributions will be licensed under the same license as PyRPOD (currently [GPL-3.0 License](LICENSE.md)). Ensure your contributions comply with third-party licensing terms if applicable. - ---- - -Thank you for contributing to PyRPOD! Your efforts help make this project a valuable resource for the community. +# Contributing to PyRPOD + +Please read carefully before contributing to PyRPOD. + +## Code Contributions + +To contribute to the PyRPOD source code, you must agree to the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). + +By submitting a pull request, you implicitly agree to the certifications and terms of the DCO. No additional actions are required. + +We aim to make our contribution process as developer-friendly as possible while maintaining high-quality standards. + +## Bug Reports + +If you identify a bug, please open an issue in the repository. Provide detailed information, including: + +- Steps to reproduce the issue. +- Expected versus actual behavior. +- Any relevant code snippets or configurations. + +Reports should ideally include minimal code examples and associated outputs to make troubleshooting easier. + +## Feature Requests + +Feature requests can be submitted as issues. While we strive to accommodate requests, keep in mind that PyRPOD is an open-source project with limited resources. We may provide guidance on implementing your requested features. + +## Contributions of Novel Features + +PyRPOD accepts contributions from community members with demonstrated programming skills and familiarity with the PyRPOD framework. Before starting significant development efforts: + +1. Review the PyRPOD documentation and style guides. +2. Engage with maintainers and the community to validate your proposal's alignment with PyRPOD's goals. + +To contribute: + +1. Fork the repository following the [forking workflow guide](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/working-with-forks). +2. Create new modules, functions, or classes instead of altering existing ones wherever possible. +3. Document your additions thoroughly, including: + - Code comments. + - References to academic papers if applicable. + - Example usage or test cases. + +When your contribution is ready, submit a pull request. Include a clear summary of your changes, their purpose, and any critical details. + +## Testing and Continuous Integration (CI) + +PyRPOD uses [pytest](https://docs.pytest.org/) to ensure stability and prevent regressions. To facilitate this: + +1. Write tests for your contributions in the `tests` directory, following the existing `__test_NN.py` naming convention (e.g. `rpod_unit_test_04.py`). +2. Run the full suite locally with `pytest` from the repository root before submitting a pull request. +3. Tests are automatically tagged with `unit`, `integration`, `verification`, and subsystem (`mdao`, `mission`, `plume`, `rpod`) markers based on their filename, so you can run a subset with e.g. `pytest -m unit` or `pytest -m rpod`. +4. CI runs the same `pytest` suite automatically on every push and pull request via [GitHub Actions](.github/workflows/tests.yml). + +## Code Formatting + +Consistency is key. PyRPOD enforces uniform code formatting using the `black` code formatter. + +- Install and run `black` on your code before committing: + ```bash + black . + ``` +- The CI pipeline will reject pull requests with improperly formatted code. + +## Licensing + +By contributing, you agree that your contributions will be licensed under the same license as PyRPOD (currently [GPL-3.0 License](LICENSE.md)). Ensure your contributions comply with third-party licensing terms if applicable. + +--- + +Thank you for contributing to PyRPOD! Your efforts help make this project a valuable resource for the community. diff --git a/LICENSE.txt b/LICENSE.txt index 3877ae0..f288702 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,674 +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 -. + 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/case/axial_optimization/config.ini b/case/axial_optimization/config.ini index b031af4..8733123 100644 --- a/case/axial_optimization/config.ini +++ b/case/axial_optimization/config.ini @@ -1,67 +1,67 @@ - -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = lm_transformed.stl -stl_thruster = thruster_ATV216_transformed.stl -stl_cluster = cluster_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = square_plate_large_med_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History -[jfh] -jfh = jfh_blank.A -# Flight Plan - contains orbital maneuver data. -flight_plan = flight_plan_BLT.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, position, and type data. -tcf = tcf_24_thrusters.txt -# Cluster Configuration File - contains cluster orientation and position. -ccf = ccf.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 100 -# 25 degrees + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = lm_transformed.stl +stl_thruster = thruster_ATV216_transformed.stl +stl_cluster = cluster_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = square_plate_large_med_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_blank.A +# Flight Plan - contains orbital maneuver data. +flight_plan = flight_plan_BLT.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, position, and type data. +tcf = tcf_24_thrusters.txt +# Cluster Configuration File - contains cluster orientation and position. +ccf = ccf.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 100 +# 25 degrees wedge_theta = 0.436 \ No newline at end of file diff --git a/case/axial_optimization/tcd/ccf.txt b/case/axial_optimization/tcd/ccf.txt index 4f2d824..d907050 100644 --- a/case/axial_optimization/tcd/ccf.txt +++ b/case/axial_optimization/tcd/ccf.txt @@ -1,6 +1,6 @@ -4 -m -P1 0 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2 0 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3 0 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 -P4 0 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 +4 +m +P1 0 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2 0 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3 0 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 +P4 0 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 diff --git a/case/cant_optimization/config.ini b/case/cant_optimization/config.ini index 4198bdc..437631f 100644 --- a/case/cant_optimization/config.ini +++ b/case/cant_optimization/config.ini @@ -1,71 +1,71 @@ - -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = lm_transformed.stl -stl_thruster = thruster_ATV216_transformed.stl -stl_cluster = cluster_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = gateway_3hr_transformed.stl -#stl = square_plate_large_fine_defaced_transformed.stl -#stl = square_plate_large_coarse_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 0 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History -[jfh] -jfh = jfh_blank.A -# Flight Plan - contains orbital maneuver data. -flight_plan = flight_plan_BLT.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, position, and type data. -# NOTE: When changing number of thrusters, must edit rpod.edit_1d_JFH - # hardcoded active thrusters -tcf = tcf_003.txt -# Cluster Configuration File - contains cluster orientation and position. -ccf = ccf.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf_8.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 50 -# 14.3239 degrees + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = lm_transformed.stl +stl_thruster = thruster_ATV216_transformed.stl +stl_cluster = cluster_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = gateway_3hr_transformed.stl +#stl = square_plate_large_fine_defaced_transformed.stl +#stl = square_plate_large_coarse_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_blank.A +# Flight Plan - contains orbital maneuver data. +flight_plan = flight_plan_BLT.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, position, and type data. +# NOTE: When changing number of thrusters, must edit rpod.edit_1d_JFH + # hardcoded active thrusters +tcf = tcf_003.txt +# Cluster Configuration File - contains cluster orientation and position. +ccf = ccf.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf_8.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 50 +# 14.3239 degrees wedge_theta = 0.25 \ No newline at end of file diff --git a/case/cant_optimization/tcd/16_tcf_legend.txt b/case/cant_optimization/tcd/16_tcf_legend.txt index bc60819..13cd696 100644 --- a/case/cant_optimization/tcd/16_tcf_legend.txt +++ b/case/cant_optimization/tcd/16_tcf_legend.txt @@ -1,23 +1,23 @@ -16 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -# first 4 packs are going to be x/pitch/yaw -P1T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the top -P1T2 001 accel1 -P2T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the left hand side -P2T2 001 accel1 -P3T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the bottom -P3T2 001 accel1 -P4T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the right hand side -P4T2 001 accel1 -# last 4 packs are going to be y/z/roll -P5T1 001 pos -P5T2 001 neg -P6T1 001 pos -P6T2 001 neg -P7T1 001 pos -P7T2 001 neg -P8T1 001 pos -P8T2 001 neg +16 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +# first 4 packs are going to be x/pitch/yaw +P1T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the top +P1T2 001 accel1 +P2T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the left hand side +P2T2 001 accel1 +P3T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the bottom +P3T2 001 accel1 +P4T1 004 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the right hand side +P4T2 001 accel1 +# last 4 packs are going to be y/z/roll +P5T1 001 pos +P5T2 001 neg +P6T1 001 pos +P6T2 001 neg +P7T1 001 pos +P7T2 001 neg +P8T1 001 pos +P8T2 001 neg 0 \ No newline at end of file diff --git a/case/cant_optimization/tcd/ccf.txt b/case/cant_optimization/tcd/ccf.txt index ef2bca0..ebcc598 100644 --- a/case/cant_optimization/tcd/ccf.txt +++ b/case/cant_optimization/tcd/ccf.txt @@ -1,6 +1,6 @@ -4 -m -P1 -8.194 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2 -8.194 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3 -8.194 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 -P4 -8.194 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 +4 +m +P1 -8.194 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2 -8.194 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3 -8.194 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 +P4 -8.194 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 diff --git a/case/cant_optimization/tcd/tcf_004.txt b/case/cant_optimization/tcd/tcf_004.txt index d1c8b3f..803b778 100644 --- a/case/cant_optimization/tcd/tcf_004.txt +++ b/case/cant_optimization/tcd/tcf_004.txt @@ -1,18 +1,18 @@ -13 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -MET1 006 0 0 0 0 0 0 0 0 0 0 0 0 -P1T1 004 -0.104313 0 0.11205 1 0 0 0 1 0 0 0 1 -P1T2 004 0 0 0 1 0 0 0 1 0 0 0 1 -P2T1 004 -0.104313 0.11205 0 1 0 0 0 1 0 0 0 1 -P2T2 004 0 0 0 0 0 0 0 0 0 0 0 0 -P3T1 004 -0.104313 0 -0.11205 1 0 0 0 1 0 0 0 1 -P3T2 004 0 0 0 0 0 0 0 0 0 0 0 0 -P4T1 004 -0.104313 -0.11205 0 1 0 0 0 1 0 0 0 1 -P4T2 004 0 0 0 0 0 0 0 0 0 0 0 0 -P5T1 003 0 0 0 0 0 0 0 0 0 0 0 0 -P5T2 003 0 0 0 0 0 0 0 0 0 0 0 0 -P6T1 003 0 0 0 0 0 0 0 0 0 0 0 0 -P6T2 003 0 0 0 0 0 0 0 0 0 0 0 0 +13 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +MET1 006 0 0 0 0 0 0 0 0 0 0 0 0 +P1T1 004 -0.104313 0 0.11205 1 0 0 0 1 0 0 0 1 +P1T2 004 0 0 0 1 0 0 0 1 0 0 0 1 +P2T1 004 -0.104313 0.11205 0 1 0 0 0 1 0 0 0 1 +P2T2 004 0 0 0 0 0 0 0 0 0 0 0 0 +P3T1 004 -0.104313 0 -0.11205 1 0 0 0 1 0 0 0 1 +P3T2 004 0 0 0 0 0 0 0 0 0 0 0 0 +P4T1 004 -0.104313 -0.11205 0 1 0 0 0 1 0 0 0 1 +P4T2 004 0 0 0 0 0 0 0 0 0 0 0 0 +P5T1 003 0 0 0 0 0 0 0 0 0 0 0 0 +P5T2 003 0 0 0 0 0 0 0 0 0 0 0 0 +P6T1 003 0 0 0 0 0 0 0 0 0 0 0 0 +P6T2 003 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/case/cant_optimization/tcd/tcf_005.txt b/case/cant_optimization/tcd/tcf_005.txt index 723bdfe..f1dbe1d 100644 --- a/case/cant_optimization/tcd/tcf_005.txt +++ b/case/cant_optimization/tcd/tcf_005.txt @@ -1,18 +1,18 @@ -13 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -MET1 006 0 0 0 0 0 0 0 0 0 0 0 0 -P1T1 005 -0.104313 0 0.11205 1 0 0 0 1 0 0 0 1 -P1T2 005 0 0 0 1 0 0 0 1 0 0 0 1 -P2T1 005 -0.104313 0.11205 0 1 0 0 0 1 0 0 0 1 -P2T2 005 0 0 0 0 0 0 0 0 0 0 0 0 -P3T1 005 -0.104313 0 -0.11205 1 0 0 0 1 0 0 0 1 -P3T2 005 0 0 0 0 0 0 0 0 0 0 0 0 -P4T1 005 -0.104313 -0.11205 0 1 0 0 0 1 0 0 0 1 -P4T2 005 0 0 0 0 0 0 0 0 0 0 0 0 -P5T1 003 0 0 0 0 0 0 0 0 0 0 0 0 -P5T2 003 0 0 0 0 0 0 0 0 0 0 0 0 -P6T1 003 0 0 0 0 0 0 0 0 0 0 0 0 -P6T2 003 0 0 0 0 0 0 0 0 0 0 0 0 +13 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +MET1 006 0 0 0 0 0 0 0 0 0 0 0 0 +P1T1 005 -0.104313 0 0.11205 1 0 0 0 1 0 0 0 1 +P1T2 005 0 0 0 1 0 0 0 1 0 0 0 1 +P2T1 005 -0.104313 0.11205 0 1 0 0 0 1 0 0 0 1 +P2T2 005 0 0 0 0 0 0 0 0 0 0 0 0 +P3T1 005 -0.104313 0 -0.11205 1 0 0 0 1 0 0 0 1 +P3T2 005 0 0 0 0 0 0 0 0 0 0 0 0 +P4T1 005 -0.104313 -0.11205 0 1 0 0 0 1 0 0 0 1 +P4T2 005 0 0 0 0 0 0 0 0 0 0 0 0 +P5T1 003 0 0 0 0 0 0 0 0 0 0 0 0 +P5T2 003 0 0 0 0 0 0 0 0 0 0 0 0 +P6T1 003 0 0 0 0 0 0 0 0 0 0 0 0 +P6T2 003 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/case/flow_visualization/config.ini b/case/flow_visualization/config.ini index 752a8dc..95904ed 100644 --- a/case/flow_visualization/config.ini +++ b/case/flow_visualization/config.ini @@ -1,70 +1,70 @@ - -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = lm_transformed.stl -stl_thruster = thruster_ATV216_transformed.stl -stl_cluster = cluster_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = square_plate_large_fine_defaced_series_transformed.stl -#stl = square_plate_large_coarse_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 0 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History -[jfh] -jfh = jfh_flow_vis.A -# Flight Plan - contains orbital maneuver data. -flight_plan = flight_plan_BLT.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, position, and type data. -# NOTE: When changing number of thrusters, must edit rpod.edit_1d_JFH - # hardcoded active thrusters -tcf = tcf_BLT.txt -# Cluster Configuration File - contains cluster orientation and position. -ccf = ccf.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf_8.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 50 -# 57 degrees + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = lm_transformed.stl +stl_thruster = thruster_ATV216_transformed.stl +stl_cluster = cluster_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = square_plate_large_fine_defaced_series_transformed.stl +#stl = square_plate_large_coarse_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_flow_vis.A +# Flight Plan - contains orbital maneuver data. +flight_plan = flight_plan_BLT.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, position, and type data. +# NOTE: When changing number of thrusters, must edit rpod.edit_1d_JFH + # hardcoded active thrusters +tcf = tcf_BLT.txt +# Cluster Configuration File - contains cluster orientation and position. +ccf = ccf.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf_8.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 50 +# 57 degrees wedge_theta = 1 \ No newline at end of file diff --git a/case/flow_visualization/tcd/ccf.txt b/case/flow_visualization/tcd/ccf.txt index ef2bca0..ebcc598 100644 --- a/case/flow_visualization/tcd/ccf.txt +++ b/case/flow_visualization/tcd/ccf.txt @@ -1,6 +1,6 @@ -4 -m -P1 -8.194 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2 -8.194 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3 -8.194 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 -P4 -8.194 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 +4 +m +P1 -8.194 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2 -8.194 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3 -8.194 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 +P4 -8.194 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 diff --git a/case/mdao/cant_sweep/config.ini b/case/mdao/cant_sweep/config.ini index 18359f0..6815d56 100644 --- a/case/mdao/cant_sweep/config.ini +++ b/case/mdao/cant_sweep/config.ini @@ -1,66 +1,66 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = logistics_module_transformed.stl -stl_thruster = thruster_ATV216_transformed.stl -stl_cluster = cluster_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = square_plate_large_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History -[jfh] -jfh = jfh_blank.A -# Flight Plan - contains orbital maneuver data. -flight_plan = flight_plan_BLT.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, position, and type data. -tcf = tcf.txt -# Cluster Configuration File - contains cluster orientation and position. -ccf = ccf.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 100 -# 25 degrees +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = logistics_module_transformed.stl +stl_thruster = thruster_ATV216_transformed.stl +stl_cluster = cluster_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = square_plate_large_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +jfh = jfh_blank.A +# Flight Plan - contains orbital maneuver data. +flight_plan = flight_plan_BLT.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, position, and type data. +tcf = tcf.txt +# Cluster Configuration File - contains cluster orientation and position. +ccf = ccf.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 100 +# 25 degrees wedge_theta = 0.436 \ No newline at end of file diff --git a/case/mdao/trade_study/config.ini b/case/mdao/trade_study/config.ini index ff91839..2cc2326 100644 --- a/case/mdao/trade_study/config.ini +++ b/case/mdao/trade_study/config.ini @@ -1,61 +1,61 @@ -# STL files of the Target and Visiting Vehicle for RPOD analysis. -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_high_res_transformed.stl - -[tv] -stl = square_plate_low_res_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_1d_approach.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_4_thrusters.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - - -# Parameters for scaling plume geometry. -[plume] -radius = 50 -# wedge angle of 20 degrees +# STL files of the Target and Visiting Vehicle for RPOD analysis. +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_high_res_transformed.stl + +[tv] +stl = square_plate_low_res_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_1d_approach.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_4_thrusters.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + + +# Parameters for scaling plume geometry. +[plume] +radius = 50 +# wedge angle of 20 degrees wedge_theta = 0.349 \ No newline at end of file diff --git a/case/mission/flight_envelopes/config.ini b/case/mission/flight_envelopes/config.ini index 070952d..5513543 100644 --- a/case/mission/flight_envelopes/config.ini +++ b/case/mission/flight_envelopes/config.ini @@ -1,32 +1,32 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = cylinder.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = flat_plate.stl - -[pm] -# Gas kinetics model -kinetics = None - -# Jet Firing History (could add here) -[jfh] -jfh = JFH.A -flight_plan = flight_plan.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_flight_envelopes.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - - -# Parameters for scaling plume geometry. -[plume] -radius = 25 -wedge_theta = 0.08725 # 5 degrees - - +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = flat_plate.stl + +[pm] +# Gas kinetics model +kinetics = None + +# Jet Firing History (could add here) +[jfh] +jfh = JFH.A +flight_plan = flight_plan.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_flight_envelopes.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +wedge_theta = 0.08725 # 5 degrees + + diff --git a/case/mission/fuel_calc/config.ini b/case/mission/fuel_calc/config.ini index d538eac..ee00275 100644 --- a/case/mission/fuel_calc/config.ini +++ b/case/mission/fuel_calc/config.ini @@ -1,65 +1,65 @@ - -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = logistics_module_transformed.stl -stl_thruster = thruster_ATV216_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = convex_tv_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History -[jfh] -# Jet Firing History - contains jet firings and the orientation and position data for the LM. -jfh = jfh.A -# Flight Plan - contains orbital maneuver data. -flight_plan = flight_plan_BLT.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, position, and type data. -tcf = tcf_003.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf_8.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 30 -# 25 degrees + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = logistics_module_transformed.stl +stl_thruster = thruster_ATV216_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = convex_tv_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History +[jfh] +# Jet Firing History - contains jet firings and the orientation and position data for the LM. +jfh = jfh.A +# Flight Plan - contains orbital maneuver data. +flight_plan = flight_plan_BLT.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, position, and type data. +tcf = tcf_003.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf_8.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 30 +# 25 degrees wedge_theta = 0.436 \ No newline at end of file diff --git a/case/mission/fuel_calc/jfh/flight_plan_DLT.csv b/case/mission/fuel_calc/jfh/flight_plan_DLT.csv index 813f434..0f207fc 100644 --- a/case/mission/fuel_calc/jfh/flight_plan_DLT.csv +++ b/case/mission/fuel_calc/jfh/flight_plan_DLT.csv @@ -1,6 +1,6 @@ -firing, ooo, mae, me, ae, vy_pos, vy_neg, vz_pos, vz_neg, wy_pos, wy_neg, wp_pos, wp_neg, wr_pos, wr_neg -1, 2, 178, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -2, 1, 250.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -3, 0, 0, 0, 2.8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0087, 0.0087, 0, 0 +firing, ooo, mae, me, ae, vy_pos, vy_neg, vz_pos, vz_neg, wy_pos, wy_neg, wp_pos, wp_neg, wr_pos, wr_neg +1, 2, 178, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +2, 1, 250.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +3, 0, 0, 0, 2.8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0087, 0.0087, 0, 0 5, 4, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 \ No newline at end of file diff --git a/case/mission/fuel_calc/tcd/ccf.txt b/case/mission/fuel_calc/tcd/ccf.txt index 0c81cf2..a50eb3e 100644 --- a/case/mission/fuel_calc/tcd/ccf.txt +++ b/case/mission/fuel_calc/tcd/ccf.txt @@ -1,6 +1,6 @@ -4 -m -P1 -10.734 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2 -10.734 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3 -10.734 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 -P4 -10.734 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 +4 +m +P1 -10.734 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2 -10.734 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3 -10.734 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 +P4 -10.734 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 diff --git a/case/mission/fuel_calc/tcd/tcf_DLT.txt b/case/mission/fuel_calc/tcd/tcf_DLT.txt index b5e011e..093bdad 100644 --- a/case/mission/fuel_calc/tcd/tcf_DLT.txt +++ b/case/mission/fuel_calc/tcd/tcf_DLT.txt @@ -1,38 +1,38 @@ -33 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -MET1 008 0 0 0 0 0 0 0 0 0 0 0 0 -MET2 008 0 0 0 0 0 0 0 0 0 0 0 0 -MET3 008 0 0 0 0 0 0 0 0 0 0 0 0 -AET1 006 0 0 0 0 0 0 0 0 0 0 0 0 -AET2 006 0 0 0 0 0 0 0 0 0 0 0 0 -AET3 006 0 0 0 0 0 0 0 0 0 0 0 0 -AET4 006 0 0 0 0 0 0 0 0 0 0 0 0 -AET5 006 0 0 0 0 0 0 0 0 0 0 0 0 -AET6 006 0 0 0 0 0 0 0 0 0 0 0 0 -P1T1 001 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T2 001 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T3 001 0 0 0 1 0 0 0 1 0 0 0 1 -P1T4 001 0 0 0 1 0 0 0 1 0 0 0 1 -P2T1 001 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P2T2 001 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P2T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P2T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P3T1 001 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T2 001 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P3T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P4T1 001 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P4T2 001 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P4T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P4T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P7T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P7T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P8T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P8T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +33 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +MET1 008 0 0 0 0 0 0 0 0 0 0 0 0 +MET2 008 0 0 0 0 0 0 0 0 0 0 0 0 +MET3 008 0 0 0 0 0 0 0 0 0 0 0 0 +AET1 006 0 0 0 0 0 0 0 0 0 0 0 0 +AET2 006 0 0 0 0 0 0 0 0 0 0 0 0 +AET3 006 0 0 0 0 0 0 0 0 0 0 0 0 +AET4 006 0 0 0 0 0 0 0 0 0 0 0 0 +AET5 006 0 0 0 0 0 0 0 0 0 0 0 0 +AET6 006 0 0 0 0 0 0 0 0 0 0 0 0 +P1T1 001 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T2 001 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T3 001 0 0 0 1 0 0 0 1 0 0 0 1 +P1T4 001 0 0 0 1 0 0 0 1 0 0 0 1 +P2T1 001 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P2T2 001 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P2T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P2T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P3T1 001 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T2 001 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P3T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P4T1 001 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P4T2 001 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P4T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P4T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P7T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P7T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P8T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P8T2 001 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/case/mission/fuel_calc/tcd/tcf_DLT_legend.txt b/case/mission/fuel_calc/tcd/tcf_DLT_legend.txt index e9497fa..d29df93 100644 --- a/case/mission/fuel_calc/tcd/tcf_DLT_legend.txt +++ b/case/mission/fuel_calc/tcd/tcf_DLT_legend.txt @@ -1,40 +1,40 @@ -33 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -MET1 008 -MET2 008 -MET3 008 -AET1 006 -AET2 006 -AET3 006 -AET4 006 -AET5 006 -AET6 006 -# first 4 packs are going to be pitch/yaw -P1T1 001 decel1 looking in +x dir, this is the left hand side -P1T2 001 decel2 -P1T3 001 accel1 -P1T4 001 accel2 -P2T1 001 decel1 -P2T2 001 decel2 -P2T3 001 accel1 -P2T4 001 accel2 -P3T1 001 decel1 -P3T2 001 decel2 -P3T3 001 accel1 -P3T4 001 accel2 -P4T1 001 decel1 -P4T2 001 decel2 -P4T3 001 accel1 -P4T4 001 accel2 -# last 4 packs are going to be roll/y/z -P5T1 001 pos -P5T2 001 neg -P6T1 001 pos -P6T2 001 neg -P7T1 001 pos -P7T2 001 neg -P8T1 001 pos -P8T2 001 neg +33 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +MET1 008 +MET2 008 +MET3 008 +AET1 006 +AET2 006 +AET3 006 +AET4 006 +AET5 006 +AET6 006 +# first 4 packs are going to be pitch/yaw +P1T1 001 decel1 looking in +x dir, this is the left hand side +P1T2 001 decel2 +P1T3 001 accel1 +P1T4 001 accel2 +P2T1 001 decel1 +P2T2 001 decel2 +P2T3 001 accel1 +P2T4 001 accel2 +P3T1 001 decel1 +P3T2 001 decel2 +P3T3 001 accel1 +P3T4 001 accel2 +P4T1 001 decel1 +P4T2 001 decel2 +P4T3 001 accel1 +P4T4 001 accel2 +# last 4 packs are going to be roll/y/z +P5T1 001 pos +P5T2 001 neg +P6T1 001 pos +P6T2 001 neg +P7T1 001 pos +P7T2 001 neg +P8T1 001 pos +P8T2 001 neg 0 \ No newline at end of file diff --git a/case/mission/plume_constraint_case/config.ini b/case/mission/plume_constraint_case/config.ini index 5b250b0..54c5a48 100644 --- a/case/mission/plume_constraint_case/config.ini +++ b/case/mission/plume_constraint_case/config.ini @@ -1,61 +1,61 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = square_plate_low_res_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_plume_constraint_case.A -flight_plan = flight_plan.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_16_thrusters.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 25 -# 5 degrees +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = square_plate_low_res_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_plume_constraint_case.A +flight_plan = flight_plan.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_16_thrusters.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# 5 degrees wedge_theta = 0.08725 \ No newline at end of file diff --git a/case/plume/plume_case/config.ini b/case/plume/plume_case/config.ini index ea658ed..fa16375 100644 --- a/case/plume/plume_case/config.ini +++ b/case/plume/plume_case/config.ini @@ -1,57 +1,57 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = flat_plate_low_res_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 0 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_base_case.A -flight_plan = flight_plan.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_16_thrusters.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 25 -# 25 degrees +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = flat_plate_low_res_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_base_case.A +flight_plan = flight_plan.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_16_thrusters.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# 25 degrees wedge_theta = 0.436 \ No newline at end of file diff --git a/case/plume/plume_sq_plate/config.ini b/case/plume/plume_sq_plate/config.ini index d1fd287..eef9043 100644 --- a/case/plume/plume_sq_plate/config.ini +++ b/case/plume/plume_sq_plate/config.ini @@ -1,61 +1,61 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = square_plate_low_res_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 1 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_plume_sq_plate.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_16_thrusters.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - - -# Parameters for scaling plume geometry. -[plume] -radius = 25 -# 25 deg +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = square_plate_low_res_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 1 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_plume_sq_plate.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_16_thrusters.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# 25 deg wedge_theta = 0.436 \ No newline at end of file diff --git a/case/rpod/1d_approach/config.ini b/case/rpod/1d_approach/config.ini index c234298..cc6698c 100644 --- a/case/rpod/1d_approach/config.ini +++ b/case/rpod/1d_approach/config.ini @@ -1,57 +1,57 @@ -# STL files of the Target and Visiting Vehicle for RPOD analysis. -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_high_res_transformed.stl - -[tv] -stl = square_plate_low_res_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 0 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = None - -# Gas-surface interaction model -surface_interaction = None - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_1d_approach.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_1_thruster.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - - -# Parameters for scaling plume geometry. -[plume] -radius = 50 -# wedge angle of 20 degrees +# STL files of the Target and Visiting Vehicle for RPOD analysis. +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_high_res_transformed.stl + +[tv] +stl = square_plate_low_res_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = None + +# Gas-surface interaction model +surface_interaction = None + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_1d_approach.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_1_thruster.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + + +# Parameters for scaling plume geometry. +[plume] +radius = 50 +# wedge angle of 20 degrees wedge_theta = 0.349 \ No newline at end of file diff --git a/case/rpod/base_case/config.ini b/case/rpod/base_case/config.ini index ccd944f..24c7ae9 100644 --- a/case/rpod/base_case/config.ini +++ b/case/rpod/base_case/config.ini @@ -1,32 +1,32 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = flat_plate_low_res_transformed.stl - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = None - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_base_case.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_16_thrusters.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 25 - -# wedge angle of 15 degrees +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = flat_plate_low_res_transformed.stl + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = None + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_base_case.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_16_thrusters.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 + +# wedge angle of 15 degrees wedge_theta = 0.262 \ No newline at end of file diff --git a/case/rpod/hollow_cube/config.ini b/case/rpod/hollow_cube/config.ini index dd97d69..cc65159 100644 --- a/case/rpod/hollow_cube/config.ini +++ b/case/rpod/hollow_cube/config.ini @@ -1,30 +1,30 @@ -# STL files of the Target and Visiting Vehicle for RPOD analysis. -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_transformed.stl - -[tv] -stl = hollow_cube_low_res_transformed.stl - -# kinetics and gas-surface interaction plume models -[pm] -kinetics = None - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_hollow_cube.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_16_thrusters.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - - -# Parameters for scaling plume geometry. -[plume] -radius = 100 -# wedge angle of 25 degrees +# STL files of the Target and Visiting Vehicle for RPOD analysis. +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_transformed.stl + +[tv] +stl = hollow_cube_low_res_transformed.stl + +# kinetics and gas-surface interaction plume models +[pm] +kinetics = None + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_hollow_cube.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_16_thrusters.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + + +# Parameters for scaling plume geometry. +[plume] +radius = 100 +# wedge angle of 25 degrees wedge_theta = 0.436 \ No newline at end of file diff --git a/case/rpod/koz/config.ini b/case/rpod/koz/config.ini index 5e0a7ee..c64dcee 100644 --- a/case/rpod/koz/config.ini +++ b/case/rpod/koz/config.ini @@ -1,57 +1,57 @@ -# STL files of the Target and Visiting Vehicle for RPOD analysis. -[vv] -stl_lm = cylinder_transformed.stl -stl_thruster = mold_funnel_high_res_transformed.stl - -[tv] -stl = convex_tv_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 0 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = None - -# Gas-surface interaction model -surface_interaction = None - -# Jet Firing History (could add here) -[jfh] -jfh = jfh_1d_approach.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = tcf_1_thruster.txt -# Thruster Grouping File - contains thruster groups. -tgf = tgf.ini -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - - -# Parameters for scaling plume geometry. -[plume] -radius = 50 -# wedge angle of 20 degrees +# STL files of the Target and Visiting Vehicle for RPOD analysis. +[vv] +stl_lm = cylinder_transformed.stl +stl_thruster = mold_funnel_high_res_transformed.stl + +[tv] +stl = convex_tv_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 0 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = None + +# Gas-surface interaction model +surface_interaction = None + +# Jet Firing History (could add here) +[jfh] +jfh = jfh_1d_approach.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = tcf_1_thruster.txt +# Thruster Grouping File - contains thruster groups. +tgf = tgf.ini +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + + +# Parameters for scaling plume geometry. +[plume] +radius = 50 +# wedge angle of 20 degrees wedge_theta = 0.349 \ No newline at end of file diff --git a/case/rpod/multi_surface/config.ini b/case/rpod/multi_surface/config.ini index c476dc3..5251d8a 100644 --- a/case/rpod/multi_surface/config.ini +++ b/case/rpod/multi_surface/config.ini @@ -1,52 +1,52 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl = cylinder.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = multi_plate.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# max heat flux integral (kJ/m^2) -heat_flux_load = 119.6 - -# max heat flux rate (kW/m^2) -heat_flux = 133.0 - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History (could add here) -[jfh] -jfh = JFH05.A -flight_plan = flight_plan.csv - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = TCD.txt - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 25 -# 5 degrees +# Visiting Vehicle for RPOD analysis +[vv] +stl = cylinder.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = multi_plate.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# max heat flux integral (kJ/m^2) +heat_flux_load = 119.6 + +# max heat flux rate (kW/m^2) +heat_flux = 133.0 + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History (could add here) +[jfh] +jfh = JFH05.A +flight_plan = flight_plan.csv + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = TCD.txt + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 25 +# 5 degrees wedge_theta = 0.08725 \ No newline at end of file diff --git a/case/rpod/stl_to_vtk/config.ini b/case/rpod/stl_to_vtk/config.ini index 92d90a6..7b0ea0e 100644 --- a/case/rpod/stl_to_vtk/config.ini +++ b/case/rpod/stl_to_vtk/config.ini @@ -1,30 +1,30 @@ -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = cylinder.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = 'box.stl' - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = None - -# Jet Firing History (could add here) -[jfh] -jfh = 'JFH.A' - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, grouping, and type data. -tcf = 'tcf.txt' - -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = 'tdf.csv' - - -# Parameters for scaling plume geometry. -[plume] -radius = 1 +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = cylinder.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = 'box.stl' + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = None + +# Jet Firing History (could add here) +[jfh] +jfh = 'JFH.A' + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, grouping, and type data. +tcf = 'tcf.txt' + +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = 'tdf.csv' + + +# Parameters for scaling plume geometry. +[plume] +radius = 1 wedge_theta = 10 \ No newline at end of file diff --git a/case/tcd_decoupling/config.ini b/case/tcd_decoupling/config.ini index 726dc49..43e0a35 100644 --- a/case/tcd_decoupling/config.ini +++ b/case/tcd_decoupling/config.ini @@ -1,63 +1,63 @@ - -# Visiting Vehicle for RPOD analysis -[vv] -stl_lm = logistics_module_transformed.stl -stl_thruster = thruster_ATV216_transformed.stl -stl_cluster = cluster_transformed.stl - -# Target Vehicle for RPOD analysis -[tv] -stl = convex_tv_transformed.stl - -# surface wall temperature (Kelvin) -surface_temp = 100 - -# proportion of diffuse particle reflections [0, 1] -sigma = 1 - -# check plume constraints? 0 or 1 -check_constraints = 1 - -# max heat flux integral (J/m^2) -heat_flux_load = 119600 -heat_flux_window_size = 535 - -# max heat flux rate (W/m^2) -heat_flux = 133000 - -# max pressure load -normal_pressure_load = inf -normal_pressure_window_size = inf - -# max normal pressure (N/m^2) -normal_pressure = 165.0 - -# max shear pressure (N/m^2) -shear_pressure = 36 - -# Plume kinetics and interactions models -[pm] -# Gas kinetics model -kinetics = Simplified - -# Gas-surface interaction model -surface_interaction = Maxwellian - -# Jet Firing History (could add here) -[jfh] -jfh = jfh.A - -# Thruster configuration data. -[tcd] -# Thruster Configuration File - contains thruster orientation, position, and type data. -tcf = tcf.txt -# Cluster Configuration File - contains cluster orientation and position. -ccf = ccf.txt -# Thruster Definition File - contains thruster performance data as defined by thruster type. -tdf = tdf.csv - -# Parameters for scaling plume geometry. -[plume] -radius = 30 -# 25 degrees + +# Visiting Vehicle for RPOD analysis +[vv] +stl_lm = logistics_module_transformed.stl +stl_thruster = thruster_ATV216_transformed.stl +stl_cluster = cluster_transformed.stl + +# Target Vehicle for RPOD analysis +[tv] +stl = convex_tv_transformed.stl + +# surface wall temperature (Kelvin) +surface_temp = 100 + +# proportion of diffuse particle reflections [0, 1] +sigma = 1 + +# check plume constraints? 0 or 1 +check_constraints = 1 + +# max heat flux integral (J/m^2) +heat_flux_load = 119600 +heat_flux_window_size = 535 + +# max heat flux rate (W/m^2) +heat_flux = 133000 + +# max pressure load +normal_pressure_load = inf +normal_pressure_window_size = inf + +# max normal pressure (N/m^2) +normal_pressure = 165.0 + +# max shear pressure (N/m^2) +shear_pressure = 36 + +# Plume kinetics and interactions models +[pm] +# Gas kinetics model +kinetics = Simplified + +# Gas-surface interaction model +surface_interaction = Maxwellian + +# Jet Firing History (could add here) +[jfh] +jfh = jfh.A + +# Thruster configuration data. +[tcd] +# Thruster Configuration File - contains thruster orientation, position, and type data. +tcf = tcf.txt +# Cluster Configuration File - contains cluster orientation and position. +ccf = ccf.txt +# Thruster Definition File - contains thruster performance data as defined by thruster type. +tdf = tdf.csv + +# Parameters for scaling plume geometry. +[plume] +radius = 30 +# 25 degrees wedge_theta = 0.436 \ No newline at end of file diff --git a/case/tcd_decoupling/tcd/ccf.txt b/case/tcd_decoupling/tcd/ccf.txt index be376ed..d556532 100644 --- a/case/tcd_decoupling/tcd/ccf.txt +++ b/case/tcd_decoupling/tcd/ccf.txt @@ -1,10 +1,10 @@ -8 -m -P1 0 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2 0 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3 0 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 -P4 0 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -P5 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P6 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P7 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P8 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +8 +m +P1 0 0 1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2 0 1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3 0 0 -1.68995 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 +P4 0 -1.68995 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 +P5 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P6 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P7 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P8 0 0 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 diff --git a/case/tcd_decoupling/tcd/tcf.txt b/case/tcd_decoupling/tcd/tcf.txt index 1d05c38..e9d96b1 100644 --- a/case/tcd_decoupling/tcd/tcf.txt +++ b/case/tcd_decoupling/tcd/tcf.txt @@ -1,13 +1,13 @@ -8 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 -0.104313 0.07125 0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P1T2 001 -0.104313 -0.07125 0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T1 001 -0.104313 0.11205 -0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T2 001 -0.104313 0.11205 0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T1 001 -0.104313 -0.07125 -0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T2 001 -0.104313 0.07125 -0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T1 001 -0.104313 -0.11205 0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T2 001 -0.104313 -0.11205 -0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +8 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 -0.104313 0.07125 0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P1T2 001 -0.104313 -0.07125 0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T1 001 -0.104313 0.11205 -0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T2 001 -0.104313 0.11205 0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T1 001 -0.104313 -0.07125 -0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T2 001 -0.104313 0.07125 -0.11205 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T1 001 -0.104313 -0.11205 0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T2 001 -0.104313 -0.11205 -0.07125 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0 \ No newline at end of file diff --git a/data/flight_plan/flight_plan.csv b/data/flight_plan/flight_plan.csv index 3830844..c77f12d 100644 --- a/data/flight_plan/flight_plan.csv +++ b/data/flight_plan/flight_plan.csv @@ -1,5 +1,5 @@ -firing, v0_0, v1_0, v2_0, v0_1, v1_1, v2_1, w0_0, w1_0, w2_0, w0_1, w1_1, w2_1, t_req, d_req -1, 4800, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 -2, 200, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 45, -1 -3, 20, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 +firing, v0_0, v1_0, v2_0, v0_1, v1_1, v2_1, w0_0, w1_0, w2_0, w0_1, w1_1, w2_1, t_req, d_req +1, 4800, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 +2, 200, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 45, -1 +3, 20, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 4, 2, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 \ No newline at end of file diff --git a/data/flight_plan/flight_plan_BLT.csv b/data/flight_plan/flight_plan_BLT.csv index 05cc8d8..18ef01e 100644 --- a/data/flight_plan/flight_plan_BLT.csv +++ b/data/flight_plan/flight_plan_BLT.csv @@ -1,11 +1,11 @@ -firing, ooo, vx_pos, vy_pos, vy_neg, vz_pos, vz_neg, wy_pos, wy_neg, wp_pos, wp_neg, wr_pos, wr_neg -1, 7, 19.81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -2, 6, 1.618, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -3, 5, 39.97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -4, 4, 2.417, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -5, 3, 4.372, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -6, 2, 18.07, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -7, 1, 4.222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -8, 0, 3.838, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 -9, 8, 0, 0, 0, 0, 0, 0, 0, 0.0087, 0.0087, 0, 0 +firing, ooo, vx_pos, vy_pos, vy_neg, vz_pos, vz_neg, wy_pos, wy_neg, wp_pos, wp_neg, wr_pos, wr_neg +1, 7, 19.81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +2, 6, 1.618, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +3, 5, 39.97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +4, 4, 2.417, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +5, 3, 4.372, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +6, 2, 18.07, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +7, 1, 4.222, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +8, 0, 3.838, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +9, 8, 0, 0, 0, 0, 0, 0, 0, 0.0087, 0.0087, 0, 0 10, 9, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 \ No newline at end of file diff --git a/data/flight_plan/flight_plan_m3.csv b/data/flight_plan/flight_plan_m3.csv index 87eb624..a5b4d22 100644 --- a/data/flight_plan/flight_plan_m3.csv +++ b/data/flight_plan/flight_plan_m3.csv @@ -1,6 +1,6 @@ -firing, v0_0, v1_0, v2_0, v0_1, v1_1, v2_1, w0_0, w1_0, w2_0, w0_1, w1_1, w2_1, t_req, d_req -1, 1194, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 -2, 829, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 -3, 340, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 -4, 10, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 -5, 15, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 +firing, v0_0, v1_0, v2_0, v0_1, v1_1, v2_1, w0_0, w1_0, w2_0, w0_1, w1_1, w2_1, t_req, d_req +1, 1194, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 +2, 829, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 +3, 340, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 +4, 10, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 7200, -1 +5, 15, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 diff --git a/data/flight_plan/flight_plan_m3_1.csv b/data/flight_plan/flight_plan_m3_1.csv index bb69e07..67bbc21 100644 --- a/data/flight_plan/flight_plan_m3_1.csv +++ b/data/flight_plan/flight_plan_m3_1.csv @@ -1,2 +1,2 @@ -firing, v0_0, v1_0, v2_0, v0_1, v1_1, v2_1, w0_0, w1_0, w2_0, w0_1, w1_1, w2_1, t_req, d_req -1, 10, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 +firing, v0_0, v1_0, v2_0, v0_1, v1_1, v2_1, w0_0, w1_0, w2_0, w0_1, w1_1, w2_1, t_req, d_req +1, 10, 0, 0, 0.2, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0.5 diff --git a/data/jfh/JFH00.A b/data/jfh/JFH00.A index 7ce4d9f..cd05883 100644 --- a/data/jfh/JFH00.A +++ b/data/jfh/JFH00.A @@ -1,19 +1,19 @@ -offseted 16 0 - 0.000 0.000 0.000 - 1 0.50000 1.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 75.00 75.00 75.00 1.0000 1 1 - 2 0.50000 2.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 2 - 3 0.50000 3.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 3 - 4 0.50000 4.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 4 - 5 0.50000 5.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 5 - 6 0.50000 6.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 6 - 7 0.50000 7.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 7 - 8 0.50000 8.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 8 - 9 0.50000 9.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 9 - 10 0.50000 10.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 10 - 11 0.50000 11.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 11 - 12 0.50000 12.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 12 - 13 0.50000 13.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 13 - 14 0.50000 14.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 14 - 15 0.50000 15.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 15 - 16 0.50000 16.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 16 +offseted 16 0 + 0.000 0.000 0.000 + 1 0.50000 1.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 75.00 75.00 75.00 1.0000 1 1 + 2 0.50000 2.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 2 + 3 0.50000 3.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 3 + 4 0.50000 4.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 4 + 5 0.50000 5.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 5 + 6 0.50000 6.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 6 + 7 0.50000 7.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 7 + 8 0.50000 8.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 8 + 9 0.50000 9.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 9 + 10 0.50000 10.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 10 + 11 0.50000 11.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 11 + 12 0.50000 12.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 12 + 13 0.50000 13.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 13 + 14 0.50000 14.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 14 + 15 0.50000 15.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 15 + 16 0.50000 16.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 1 16 17 0.50000 17.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 6 1 2 4 5 10 11 \ No newline at end of file diff --git a/data/jfh/JFH01.A b/data/jfh/JFH01.A index faf6d47..c4b9eb7 100644 --- a/data/jfh/JFH01.A +++ b/data/jfh/JFH01.A @@ -1,20 +1,20 @@ -offseted 18 0 - 0.000 0.000 0.000 - 1 0.5 0.5 1.0 0.08 0.272 -0.959 -0.011 0.962 0.272 0.997 -0.011 0.08 85 85 85 1.0 0 - 2 0.5 1.0 1.0 0.704 -0.456 -0.544 0.208 0.865 -0.456 0.679 0.208 0.704 80 80 80 1.0 0 - 3 0.5 1.5 1.0 0.577 0.494 0.65 -0.815 0.302 0.494 0.048 -0.815 0.577 75 75 75 1.0 0 - 4 0.5 2.0 1.0 0.167 -0.373 0.913 0.713 -0.594 -0.373 0.681 0.713 0.167 70 70 70 1.0 0 - 5 0.5 2.5 1.0 0.982 0.131 -0.132 -0.114 0.985 0.131 0.148 -0.114 0.982 65 65 65 1.0 0 - 6 0.5 3.0 1.0 0.024 0.152 -0.988 -0.002 0.988 0.152 1.0 -0.002 0.024 60 60 60 1.0 0 - 7 0.5 3.5 1.0 0.817 -0.387 -0.428 0.221 0.895 -0.387 0.533 0.221 0.817 55 55 55 1.0 0 - 8 0.5 4.0 1.0 0.445 0.497 0.745 -0.867 0.031 0.497 0.224 -0.867 0.445 50 50 50 1.0 0 - 9 0.5 4.5 1.0 0.276 -0.447 0.851 0.827 -0.34 -0.447 0.489 0.827 0.276 45 45 45 1.0 0 - 10 0.5 5.0 1.0 0.931 0.253 -0.262 -0.187 0.949 0.253 0.313 -0.187 0.931 40 40 40 1.0 0 - 11 0.5 5.5 1.0 0.0 0.022 -1.0 -0.0 1.0 0.022 1.0 -0.0 0.0 35 35 35 1.0 0 - 12 0.5 6.0 1.0 0.907 -0.29 -0.305 0.202 0.935 -0.29 0.369 0.202 0.907 30 30 30 1.0 0 - 13 0.5 6.5 1.0 0.316 0.465 0.827 -0.85 -0.249 0.465 0.422 -0.85 0.316 25 25 25 1.0 0 - 14 0.5 7.0 1.0 0.401 -0.49 0.774 0.869 -0.062 -0.49 0.289 0.869 0.401 20 20 20 1.0 0 - 15 0.5 7.5 1.0 0.85 0.357 -0.388 -0.219 0.908 0.357 0.48 -0.219 0.85 15 15 15 1.0 0 - 16 0.5 8.0 1.0 0.012 -0.11 -0.994 0.001 0.994 -0.11 1.0 0.001 0.012 10 10 10 1.0 0 - 17 0.5 8.5 1.0 0.969 -0.173 -0.176 0.143 0.974 -0.173 0.202 0.143 0.969 5 5 5 1.0 0 +offseted 18 0 + 0.000 0.000 0.000 + 1 0.5 0.5 1.0 0.08 0.272 -0.959 -0.011 0.962 0.272 0.997 -0.011 0.08 85 85 85 1.0 0 + 2 0.5 1.0 1.0 0.704 -0.456 -0.544 0.208 0.865 -0.456 0.679 0.208 0.704 80 80 80 1.0 0 + 3 0.5 1.5 1.0 0.577 0.494 0.65 -0.815 0.302 0.494 0.048 -0.815 0.577 75 75 75 1.0 0 + 4 0.5 2.0 1.0 0.167 -0.373 0.913 0.713 -0.594 -0.373 0.681 0.713 0.167 70 70 70 1.0 0 + 5 0.5 2.5 1.0 0.982 0.131 -0.132 -0.114 0.985 0.131 0.148 -0.114 0.982 65 65 65 1.0 0 + 6 0.5 3.0 1.0 0.024 0.152 -0.988 -0.002 0.988 0.152 1.0 -0.002 0.024 60 60 60 1.0 0 + 7 0.5 3.5 1.0 0.817 -0.387 -0.428 0.221 0.895 -0.387 0.533 0.221 0.817 55 55 55 1.0 0 + 8 0.5 4.0 1.0 0.445 0.497 0.745 -0.867 0.031 0.497 0.224 -0.867 0.445 50 50 50 1.0 0 + 9 0.5 4.5 1.0 0.276 -0.447 0.851 0.827 -0.34 -0.447 0.489 0.827 0.276 45 45 45 1.0 0 + 10 0.5 5.0 1.0 0.931 0.253 -0.262 -0.187 0.949 0.253 0.313 -0.187 0.931 40 40 40 1.0 0 + 11 0.5 5.5 1.0 0.0 0.022 -1.0 -0.0 1.0 0.022 1.0 -0.0 0.0 35 35 35 1.0 0 + 12 0.5 6.0 1.0 0.907 -0.29 -0.305 0.202 0.935 -0.29 0.369 0.202 0.907 30 30 30 1.0 0 + 13 0.5 6.5 1.0 0.316 0.465 0.827 -0.85 -0.249 0.465 0.422 -0.85 0.316 25 25 25 1.0 0 + 14 0.5 7.0 1.0 0.401 -0.49 0.774 0.869 -0.062 -0.49 0.289 0.869 0.401 20 20 20 1.0 0 + 15 0.5 7.5 1.0 0.85 0.357 -0.388 -0.219 0.908 0.357 0.48 -0.219 0.85 15 15 15 1.0 0 + 16 0.5 8.0 1.0 0.012 -0.11 -0.994 0.001 0.994 -0.11 1.0 0.001 0.012 10 10 10 1.0 0 + 17 0.5 8.5 1.0 0.969 -0.173 -0.176 0.143 0.974 -0.173 0.202 0.143 0.969 5 5 5 1.0 0 18 0.5 9.0 1.0 0.201 0.401 0.894 -0.759 -0.514 0.401 0.62 -0.759 0.201 0 0 0 1.0 0 \ No newline at end of file diff --git a/data/jfh/JFH02.A b/data/jfh/JFH02.A index d5d6f19..a92eb30 100644 --- a/data/jfh/JFH02.A +++ b/data/jfh/JFH02.A @@ -1,19 +1,19 @@ -offseted 17 0 - - 1 0.50000 1.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 40.00 40.00 40.00 1.0000 1 1 - 2 0.50000 2.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 37.00 37.00 37.00 1.0000 1 2 - 3 0.50000 3.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 35.00 35.00 35.00 1.0000 1 3 - 4 0.50000 4.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 32.00 32.00 32.00 1.0000 1 4 - 5 0.50000 5.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 30.00 30.00 30.00 1.0000 1 5 - 6 0.50000 6.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 27.00 27.00 27.00 1.0000 1 6 - 7 0.50000 7.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 25.00 25.00 25.00 1.0000 1 7 - 8 0.50000 8.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 22.00 22.00 22.00 1.0000 1 8 - 9 0.50000 9.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 20.00 20.00 20.00 1.0000 1 9 - 10 0.50000 10.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 17.00 17.00 17.00 1.0000 1 10 - 11 0.50000 11.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 15.00 15.00 15.00 1.0000 1 11 - 12 0.50000 12.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 12.00 12.00 12.00 1.0000 1 12 - 13 0.50000 13.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 10.00 10.00 10.00 1.0000 1 13 - 14 0.50000 14.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 7.00 7.00 7.00 1.0000 1 14 - 15 0.50000 15.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 5.00 5.00 5.00 1.0000 1 15 - 16 0.50000 16.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 3.00 3.00 3.00 1.0000 1 16 +offseted 17 0 + + 1 0.50000 1.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 40.00 40.00 40.00 1.0000 1 1 + 2 0.50000 2.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 37.00 37.00 37.00 1.0000 1 2 + 3 0.50000 3.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 35.00 35.00 35.00 1.0000 1 3 + 4 0.50000 4.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 32.00 32.00 32.00 1.0000 1 4 + 5 0.50000 5.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 30.00 30.00 30.00 1.0000 1 5 + 6 0.50000 6.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 27.00 27.00 27.00 1.0000 1 6 + 7 0.50000 7.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 25.00 25.00 25.00 1.0000 1 7 + 8 0.50000 8.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 22.00 22.00 22.00 1.0000 1 8 + 9 0.50000 9.000000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 20.00 20.00 20.00 1.0000 1 9 + 10 0.50000 10.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 17.00 17.00 17.00 1.0000 1 10 + 11 0.50000 11.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 15.00 15.00 15.00 1.0000 1 11 + 12 0.50000 12.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 12.00 12.00 12.00 1.0000 1 12 + 13 0.50000 13.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 10.00 10.00 10.00 1.0000 1 13 + 14 0.50000 14.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 7.00 7.00 7.00 1.0000 1 14 + 15 0.50000 15.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 5.00 5.00 5.00 1.0000 1 15 + 16 0.50000 16.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 3.00 3.00 3.00 1.0000 1 16 17 0.50000 17.00000 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.00 0.00 0.00 0.00 1.0000 6 1 2 4 5 10 11 \ No newline at end of file diff --git a/data/stl/flat_plate/flat_plate.py b/data/stl/flat_plate/flat_plate.py index c93148f..99fc339 100644 --- a/data/stl/flat_plate/flat_plate.py +++ b/data/stl/flat_plate/flat_plate.py @@ -1,51 +1,51 @@ -import numpy as np -from stl import mesh - -# Define the dimensions and divisions of the flat plate -plate_width = 10.0 -plate_length = 10.0 -plate_thickness = 0.5 -division_width = 3 # Divisions in width -division_length = 3 # Divisions in length - -# Calculate increments for divisions -increment_width = plate_width / (division_width + 1) -increment_length = plate_length / (division_length + 1) - -# Create the vertices of the plate -vertices = [] -for i in range(division_width + 2): - for j in range(division_length + 2): - vertices.append([i * increment_width, j * increment_length, 0]) - vertices.append([i * increment_width, j * increment_length, plate_thickness]) - -vertices = np.array(vertices) - -# Define the triangles using vertices indices -faces = [] -for i in range(division_width + 1): - for j in range(division_length + 1): - v0 = i * (division_length + 2) + j - v1 = v0 + 1 - v2 = v0 + division_length + 3 - v3 = v0 + division_length + 2 - - faces.append([v0, v1, v2]) - faces.append([v0, v2, v3]) - -faces = np.array(faces) - -# Create the mesh -flat_plate = mesh.Mesh(np.zeros(len(faces), dtype=mesh.Mesh.dtype)) -for i, face in enumerate(faces): - for j in range(3): - flat_plate.vectors[i][j] = vertices[face[j], :] - -# Ensure outward-facing normals -flat_plate.normals[:] = np.cross( - flat_plate.vectors[:, 1] - flat_plate.vectors[:, 0], - flat_plate.vectors[:, 2] - flat_plate.vectors[:, 0] -) - -# Save the mesh to an STL file +import numpy as np +from stl import mesh + +# Define the dimensions and divisions of the flat plate +plate_width = 10.0 +plate_length = 10.0 +plate_thickness = 0.5 +division_width = 3 # Divisions in width +division_length = 3 # Divisions in length + +# Calculate increments for divisions +increment_width = plate_width / (division_width + 1) +increment_length = plate_length / (division_length + 1) + +# Create the vertices of the plate +vertices = [] +for i in range(division_width + 2): + for j in range(division_length + 2): + vertices.append([i * increment_width, j * increment_length, 0]) + vertices.append([i * increment_width, j * increment_length, plate_thickness]) + +vertices = np.array(vertices) + +# Define the triangles using vertices indices +faces = [] +for i in range(division_width + 1): + for j in range(division_length + 1): + v0 = i * (division_length + 2) + j + v1 = v0 + 1 + v2 = v0 + division_length + 3 + v3 = v0 + division_length + 2 + + faces.append([v0, v1, v2]) + faces.append([v0, v2, v3]) + +faces = np.array(faces) + +# Create the mesh +flat_plate = mesh.Mesh(np.zeros(len(faces), dtype=mesh.Mesh.dtype)) +for i, face in enumerate(faces): + for j in range(3): + flat_plate.vectors[i][j] = vertices[face[j], :] + +# Ensure outward-facing normals +flat_plate.normals[:] = np.cross( + flat_plate.vectors[:, 1] - flat_plate.vectors[:, 0], + flat_plate.vectors[:, 2] - flat_plate.vectors[:, 0] +) + +# Save the mesh to an STL file flat_plate.save('flat_plate_with_divisions.stl') \ No newline at end of file diff --git a/data/tcd/24_tcf_legend.txt b/data/tcd/24_tcf_legend.txt index 837f0d2..2c30829 100644 --- a/data/tcd/24_tcf_legend.txt +++ b/data/tcd/24_tcf_legend.txt @@ -1,31 +1,31 @@ -24 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -# first 4 packs are going to be x/pitch/yaw -P1T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the top -P1T2 003 decel2 -P1T3 001 accel1 -P1T4 001 accel2 -P2T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the left hand side -P2T2 003 decel2 -P2T3 001 accel1 -P2T4 001 accel2 -P3T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the bottom -P3T2 003 decel2 -P3T3 001 accel1 -P3T4 001 accel2 -P4T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the right hand side -P4T2 003 decel2 -P4T3 001 accel1 -P4T4 001 accel2 -# last 4 packs are going to be y/z/roll -P5T1 001 pos -P5T2 001 neg -P6T1 001 pos -P6T2 001 neg -P7T1 001 pos -P7T2 001 neg -P8T1 001 pos -P8T2 001 neg +24 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +# first 4 packs are going to be x/pitch/yaw +P1T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the top +P1T2 003 decel2 +P1T3 001 accel1 +P1T4 001 accel2 +P2T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the left hand side +P2T2 003 decel2 +P2T3 001 accel1 +P2T4 001 accel2 +P3T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the bottom +P3T2 003 decel2 +P3T3 001 accel1 +P3T4 001 accel2 +P4T1 003 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the right hand side +P4T2 003 decel2 +P4T3 001 accel1 +P4T4 001 accel2 +# last 4 packs are going to be y/z/roll +P5T1 001 pos +P5T2 001 neg +P6T1 001 pos +P6T2 001 neg +P7T1 001 pos +P7T2 001 neg +P8T1 001 pos +P8T2 001 neg 0 \ No newline at end of file diff --git a/data/tcd/TCD.txt b/data/tcd/TCD.txt index 40964c6..173addd 100644 --- a/data/tcd/TCD.txt +++ b/data/tcd/TCD.txt @@ -1,21 +1,21 @@ -16 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P1T2 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P1T3 001 -1.000000e+00 1.484900e+00 1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P1T4 001 -1.000000e+00 1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P2T1 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P2T2 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P2T3 001 -1.000000e+00 -1.484900e+00 1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P2T4 001 -1.000000e+00 -1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P3T1 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3T2 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P3T3 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P3T4 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P4T1 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P4T2 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P4T3 001 -1.000000e+00 1.484900e+00 -1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P4T4 001 -1.000000e+00 1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +16 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P1T2 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P1T3 001 -1.000000e+00 1.484900e+00 1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P1T4 001 -1.000000e+00 1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P2T1 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P2T2 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P2T3 001 -1.000000e+00 -1.484900e+00 1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P2T4 001 -1.000000e+00 -1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P3T1 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3T2 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P3T3 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P3T4 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P4T1 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P4T2 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P4T3 001 -1.000000e+00 1.484900e+00 -1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P4T4 001 -1.000000e+00 1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 0 \ No newline at end of file diff --git a/data/tcd/tcf_003.txt b/data/tcd/tcf_003.txt index 0a7b5c2..27d1df2 100644 --- a/data/tcd/tcf_003.txt +++ b/data/tcd/tcf_003.txt @@ -1,30 +1,30 @@ -25 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -MET1 006 0 0 0 0 0 0 0 0 0 0 0 0 -P1T1 003 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T2 003 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T3 003 0 0 0 1 0 0 0 1 0 0 0 1 -P1T4 003 0 0 0 1 0 0 0 1 0 0 0 1 -P2T1 003 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P2T2 003 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P2T3 003 0 0 0 0 0 0 0 0 0 0 0 0 -P2T4 003 0 0 0 0 0 0 0 0 0 0 0 0 -P3T1 003 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T2 003 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T3 003 0 0 0 0 0 0 0 0 0 0 0 0 -P3T4 003 0 0 0 0 0 0 0 0 0 0 0 0 -P4T1 003 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P4T2 003 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P4T3 003 0 0 0 0 0 0 0 0 0 0 0 0 -P4T4 003 0 0 0 0 0 0 0 0 0 0 0 0 -P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +25 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +MET1 006 0 0 0 0 0 0 0 0 0 0 0 0 +P1T1 003 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T2 003 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T3 003 0 0 0 1 0 0 0 1 0 0 0 1 +P1T4 003 0 0 0 1 0 0 0 1 0 0 0 1 +P2T1 003 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P2T2 003 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P2T3 003 0 0 0 0 0 0 0 0 0 0 0 0 +P2T4 003 0 0 0 0 0 0 0 0 0 0 0 0 +P3T1 003 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T2 003 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T3 003 0 0 0 0 0 0 0 0 0 0 0 0 +P3T4 003 0 0 0 0 0 0 0 0 0 0 0 0 +P4T1 003 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P4T2 003 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P4T3 003 0 0 0 0 0 0 0 0 0 0 0 0 +P4T4 003 0 0 0 0 0 0 0 0 0 0 0 0 +P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T4 001 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/data/tcd/tcf_16_thrusters.txt b/data/tcd/tcf_16_thrusters.txt index 755944e..c135e1d 100644 --- a/data/tcd/tcf_16_thrusters.txt +++ b/data/tcd/tcf_16_thrusters.txt @@ -1,21 +1,21 @@ -16 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 0 0 2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P1T2 001 0 0 2.07544 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P1T3 001 0 0 2.07544 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P1T4 001 0 0 2.07544 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T1 001 0 2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T2 001 0 2.07544 0 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T3 001 0 2.07544 0 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T4 001 0 2.07544 0 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T1 001 0 0 -2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T2 001 0 0 -2.07544 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T3 001 0 0 -2.07544 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T4 001 0 0 -2.07544 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T1 001 0 -2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T2 001 0 -2.07544 0 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T3 001 0 -2.07544 0 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T4 001 0 -2.07544 0 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +16 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 0 0 2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P1T2 001 0 0 2.07544 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P1T3 001 0 0 2.07544 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P1T4 001 0 0 2.07544 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T1 001 0 2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T2 001 0 2.07544 0 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T3 001 0 2.07544 0 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T4 001 0 2.07544 0 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T1 001 0 0 -2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T2 001 0 0 -2.07544 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T3 001 0 0 -2.07544 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T4 001 0 0 -2.07544 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T1 001 0 -2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T2 001 0 -2.07544 0 0.000000e+00 1.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T3 001 0 -2.07544 0 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T4 001 0 -2.07544 0 0.000000e+00 -1.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0 \ No newline at end of file diff --git a/data/tcd/tcf_1_thruster.txt b/data/tcd/tcf_1_thruster.txt index cb0ba3f..4ef05bb 100644 --- a/data/tcd/tcf_1_thruster.txt +++ b/data/tcd/tcf_1_thruster.txt @@ -1,6 +1,6 @@ -1 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 0 0 0 1 0 0 0 1 0 0 0 1 +1 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 0 0 0 1 0 0 0 1 0 0 0 1 0 \ No newline at end of file diff --git a/data/tcd/tcf_24_thrusters.txt b/data/tcd/tcf_24_thrusters.txt index f806313..83c4365 100644 --- a/data/tcd/tcf_24_thrusters.txt +++ b/data/tcd/tcf_24_thrusters.txt @@ -1,29 +1,29 @@ -24 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T2 001 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T3 001 0 0 0 1 0 0 0 1 0 0 0 1 -P1T4 001 0 0 0 1 0 0 0 1 0 0 0 1 -P2T1 001 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P2T2 001 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P2T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P2T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P3T1 001 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T2 001 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P3T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P4T1 001 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P4T2 001 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P4T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P4T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P7T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P7T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P8T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P8T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +24 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T2 001 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T3 001 0 0 0 1 0 0 0 1 0 0 0 1 +P1T4 001 0 0 0 1 0 0 0 1 0 0 0 1 +P2T1 001 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P2T2 001 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P2T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P2T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P3T1 001 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T2 001 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P3T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P4T1 001 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P4T2 001 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P4T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P4T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P7T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P7T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P8T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P8T2 001 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/data/tcd/tcf_4_thrusters.txt b/data/tcd/tcf_4_thrusters.txt index 82ec070..8e00678 100644 --- a/data/tcd/tcf_4_thrusters.txt +++ b/data/tcd/tcf_4_thrusters.txt @@ -1,9 +1,9 @@ -4 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 0 0 2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P2T1 001 0 2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P3T1 001 0 0 -2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 -P4T1 001 0 -2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +4 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 0 0 2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P2T1 001 0 2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P3T1 001 0 0 -2.07544 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 +P4T1 001 0 -2.07544 0 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 1.000000e+00 0 \ No newline at end of file diff --git a/data/tcd/tcf_BLT.txt b/data/tcd/tcf_BLT.txt index 7b98d50..7c73123 100644 --- a/data/tcd/tcf_BLT.txt +++ b/data/tcd/tcf_BLT.txt @@ -1,29 +1,29 @@ -24 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 003 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T2 003 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 -P1T3 001 0 0 0 1 0 0 0 1 0 0 0 1 -P1T4 001 0 0 0 1 0 0 0 1 0 0 0 1 -P2T1 003 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P2T2 003 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P2T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P2T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P3T1 003 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T2 003 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 -P3T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P3T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P4T1 003 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 -P4T2 003 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 -P4T3 001 0 0 0 0 0 0 0 0 0 0 0 0 -P4T4 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P7T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P7T2 001 0 0 0 0 0 0 0 0 0 0 0 0 -P8T1 001 0 0 0 0 0 0 0 0 0 0 0 0 -P8T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +24 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 003 -0.104313 0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T2 003 -0.104313 -0.07125 0.11205 1 0 0 0 1 0 0 0 1 +P1T3 001 0 0 0 1 0 0 0 1 0 0 0 1 +P1T4 001 0 0 0 1 0 0 0 1 0 0 0 1 +P2T1 003 -0.104313 0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P2T2 003 -0.104313 0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P2T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P2T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P3T1 003 -0.104313 -0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T2 003 -0.104313 0.07125 -0.11205 1 0 0 0 1 0 0 0 1 +P3T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P3T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P4T1 003 -0.104313 -0.11205 0.07125 1 0 0 0 1 0 0 0 1 +P4T2 003 -0.104313 -0.11205 -0.07125 1 0 0 0 1 0 0 0 1 +P4T3 001 0 0 0 0 0 0 0 0 0 0 0 0 +P4T4 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P5T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P6T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P7T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P7T2 001 0 0 0 0 0 0 0 0 0 0 0 0 +P8T1 001 0 0 0 0 0 0 0 0 0 0 0 0 +P8T2 001 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/data/tcd/tcf_BLT_legend.txt b/data/tcd/tcf_BLT_legend.txt index eb636ab..9b64b86 100644 --- a/data/tcd/tcf_BLT_legend.txt +++ b/data/tcd/tcf_BLT_legend.txt @@ -1,31 +1,31 @@ -24 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -# first 4 packs are going to be pitch/yaw -P1T1 001 decel1 looking in +x dir, this is the left hand side -P1T2 001 decel2 -P1T3 001 accel1 -P1T4 001 accel2 -P2T1 001 decel1 -P2T2 001 decel2 -P2T3 001 accel1 -P2T4 001 accel2 -P3T1 001 decel1 -P3T2 001 decel2 -P3T3 001 accel1 -P3T4 001 accel2 -P4T1 001 decel1 -P4T2 001 decel2 -P4T3 001 accel1 -P4T4 001 accel2 -# last 4 packs are going to be roll/y/z -P5T1 001 pos -P5T2 001 neg -P6T1 001 pos -P6T2 001 neg -P7T1 001 pos -P7T2 001 neg -P8T1 001 pos -P8T2 001 neg +24 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +# first 4 packs are going to be pitch/yaw +P1T1 001 decel1 looking in +x dir, this is the left hand side +P1T2 001 decel2 +P1T3 001 accel1 +P1T4 001 accel2 +P2T1 001 decel1 +P2T2 001 decel2 +P2T3 001 accel1 +P2T4 001 accel2 +P3T1 001 decel1 +P3T2 001 decel2 +P3T3 001 accel1 +P3T4 001 accel2 +P4T1 001 decel1 +P4T2 001 decel2 +P4T3 001 accel1 +P4T4 001 accel2 +# last 4 packs are going to be roll/y/z +P5T1 001 pos +P5T2 001 neg +P6T1 001 pos +P6T2 001 neg +P7T1 001 pos +P7T2 001 neg +P8T1 001 pos +P8T2 001 neg 0 \ No newline at end of file diff --git a/data/tcd/tcf_flight_envelopes.txt b/data/tcd/tcf_flight_envelopes.txt index e102c75..5cec87a 100644 --- a/data/tcd/tcf_flight_envelopes.txt +++ b/data/tcd/tcf_flight_envelopes.txt @@ -1,37 +1,37 @@ -32 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -P1T1 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P1T2 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P1T3 001 -1.000000e+00 1.484900e+00 1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P1T4 001 -1.000000e+00 1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P2T1 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P2T2 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P2T3 001 -1.000000e+00 -1.484900e+00 1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P2T4 001 -1.000000e+00 -1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P3T1 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P3T2 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P3T3 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P3T4 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P4T1 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P4T2 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P4T3 001 -1.000000e+00 1.484900e+00 -1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P4T4 001 -1.000000e+00 1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P5T1 001 -6.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P5T2 001 -6.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P5T3 001 -6.000000e+00 1.484900e+00 1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P5T4 001 -6.000000e+00 1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P6T1 001 -6.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P6T2 001 -6.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P6T3 001 -6.000000e+00 -1.484900e+00 1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P6T4 001 -6.000000e+00 -1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P7T1 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P7T2 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P7T3 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P7T4 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 -P8T1 001 -6.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 -P8T2 001 -6.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 -P8T3 001 -6.000000e+00 1.484900e+00 -1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 -P8T4 001 -6.000000e+00 1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +32 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +P1T1 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P1T2 001 -1.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P1T3 001 -1.000000e+00 1.484900e+00 1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P1T4 001 -1.000000e+00 1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P2T1 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P2T2 001 -1.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P2T3 001 -1.000000e+00 -1.484900e+00 1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P2T4 001 -1.000000e+00 -1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P3T1 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P3T2 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P3T3 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P3T4 001 -1.000000e+00 -1.484900e+00 -1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P4T1 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P4T2 001 -1.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P4T3 001 -1.000000e+00 1.484900e+00 -1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P4T4 001 -1.000000e+00 1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P5T1 001 -6.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P5T2 001 -6.000000e+00 1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P5T3 001 -6.000000e+00 1.484900e+00 1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P5T4 001 -6.000000e+00 1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P6T1 001 -6.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P6T2 001 -6.000000e+00 -1.484900e+00 1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P6T3 001 -6.000000e+00 -1.484900e+00 1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P6T4 001 -6.000000e+00 -1.484900e+00 1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P7T1 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P7T2 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P7T3 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 -0.000000e+00 -4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P7T4 001 -6.000000e+00 -1.484900e+00 -1.484900e+00 -7.071000e-01 -0.000000e+00 0.000000e+00 0.000000e+00 -4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 +P8T1 001 -6.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 1.000000e+00 -1.000000e+00 0.000000e+00 0.000000e+00 0.000000e+00 -1.000000e+00 0.000000e+00 +P8T2 001 -6.000000e+00 1.484900e+00 -1.484900e+00 0.000000e+00 0.000000e+00 -1.000000e+00 1.000000e+00 0.000000e+00 -0.000000e+00 0.000000e+00 -1.000000e+00 -0.000000e+00 +P8T3 001 -6.000000e+00 1.484900e+00 -1.484900e+00 -7.071000e-01 0.000000e+00 -0.000000e+00 0.000000e+00 4.999904e-01 -7.071000e-01 0.000000e+00 -4.999904e-01 -7.071000e-01 +P8T4 001 -6.000000e+00 1.484900e+00 -1.484900e+00 7.071000e-01 0.000000e+00 0.000000e+00 0.000000e+00 4.999904e-01 7.071000e-01 0.000000e+00 -4.999904e-01 7.071000e-01 0 \ No newline at end of file diff --git a/data/tcd/tcf_legend.txt b/data/tcd/tcf_legend.txt index 57b2bc3..02f9a2a 100644 --- a/data/tcd/tcf_legend.txt +++ b/data/tcd/tcf_legend.txt @@ -1,31 +1,31 @@ -24 -m -0.000000 0.000000 0.000000 -0.000000 0.000000 0.000000 -# first 4 packs are going to be x/pitch/yaw -P1T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the top -P1T2 001 decel2 -P1T3 001 accel1 -P1T4 001 accel2 -P2T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the left hand side -P2T2 001 decel2 -P2T3 001 accel1 -P2T4 001 accel2 -P3T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the bottom -P3T2 001 decel2 -P3T3 001 accel1 -P3T4 001 accel2 -P4T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the right hand side -P4T2 001 decel2 -P4T3 001 accel1 -P4T4 001 accel2 -# last 4 packs are going to be y/z/roll -P5T1 001 pos -P5T2 001 neg -P6T1 001 pos -P6T2 001 neg -P7T1 001 pos -P7T2 001 neg -P8T1 001 pos -P8T2 001 neg +24 +m +0.000000 0.000000 0.000000 +0.000000 0.000000 0.000000 +# first 4 packs are going to be x/pitch/yaw +P1T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the top +P1T2 001 decel2 +P1T3 001 accel1 +P1T4 001 accel2 +P2T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the left hand side +P2T2 001 decel2 +P2T3 001 accel1 +P2T4 001 accel2 +P3T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the bottom +P3T2 001 decel2 +P3T3 001 accel1 +P3T4 001 accel2 +P4T1 001 decel1 looking in +x dir towards the Gateway with +z going up, this pack is on the right hand side +P4T2 001 decel2 +P4T3 001 accel1 +P4T4 001 accel2 +# last 4 packs are going to be y/z/roll +P5T1 001 pos +P5T2 001 neg +P6T1 001 pos +P6T2 001 neg +P7T1 001 pos +P7T2 001 neg +P8T1 001 pos +P8T2 001 neg 0 \ No newline at end of file diff --git a/markdown/CODE_REVIEW_pyrpod_2025-09-18.md b/markdown/CODE_REVIEW_pyrpod_2025-09-18.md index 13b6352..a78375c 100644 --- a/markdown/CODE_REVIEW_pyrpod_2025-09-18.md +++ b/markdown/CODE_REVIEW_pyrpod_2025-09-18.md @@ -1,174 +1,174 @@ -# PyRPOD `pyrpod/` Code Review — DRY and Maintainability - -Date: 2025-09-18 -Scope: All Python under `pyrpod/` (mission, rpod, vehicle, mdao, orbital, plume, util) - -## What I looked at -- Packages: mission, rpod, vehicle, plume, mdao, orbital, util -- Representative files: - - `mission/`: `MissionPlanner.py`, `six_dof_dynamics.py`, `flight_eval.py`, `MissionEnvironment.py` - - `rpod/`: `RPOD.py`, `JetFiringHistory.py` - - `vehicle/`: `Vehicle.py`, `VisitingVehicle.py`, `LogisticsModule.py` - - `mdao/`: `SweepConfig.py` - - `plume/`: `RarefiedPlumeGasKinetics.py`, `IsentropicExpansion.py` - - `util/`: `io/file_print.py`, `stl/transform_stl.py` - -## Highlights (what’s working well) -- Clear domain structure: mission, rpod, vehicle, plume, mdao are separated logically. -- Good docstrings throughout, with parameter/return descriptions that help future readers. -- Config-driven design via `configparser` keeps cases flexible without code changes. -- Useful utilities: - - JFH I/O helpers in `util/io/file_print.py` with tests referring to them. - - `Vehicle.convert_stl_to_vtk*` centralizes mesh-to-VTK conversion. -- Physics/maths organization: - - Plume models in `plume/` are self-contained and reasonably documented. - - Orbital calculations are isolated in `orbital/HohmannTransfer.py`. -- Encapsulation: - - `MissionEnvironment` wraps case setup (vehicles, JFH) and offers logging slots for results. - - `MissionPlanner` composes submodules and suggests a clean orchestration point. - -## DRY and maintainability concerns - -1) Duplicated helpers and logic -- rotation_matrix_from_vectors duplicated - - Defined in `rpod/RPOD.py` and `rpod/JetFiringHistory.py`, and used in many places. This belongs in a single utility (e.g., `pyrpod/util/math/transform.py`). -- Near-identical STL/mesh routines - - Similar logic in `RPOD.graph_jfh` and `RPOD.visualize_sweep` (build VV mesh, rotate/translate, build plume meshes, optional clusters, save STL). This can be a single function with options. - - Multiple places create results directories and save assets using repeated os.path checks; could be factored into a small FS helper. -- Repeated “plume transformation pipeline” - - Sequence: DCM for thruster, rotate plume, rotate with VV orientation, translate by VV position, then cluster offset, then thruster exit. This appears in multiple methods; factor into a utility. -- Trans/rot performance calculations repeated - - `mission/six_dof_dynamics.py` and `mission/flight_eval.py` both have `calc_trans_performance` with identical logic. Consolidate into dynamics or a helper. - -2) Mixed responsibilities and large classes -- `rpod/RPOD.py` is very large and mixes: - - Visualization (matplotlib creation, saving images) - - STL composition and file I/O - - Physics (1D approach, delta-v computation, time multiplier) - - Plume strikes computation and export - - This should be split into smaller services/modules. As-is, it’s hard to test and evolve. - -3) Inconsistent data types and old NumPy patterns -- Use of `np.matrix` (discouraged) vs `np.array`. Mixing `np.matrix` vs `np.array` with `.A` indexing in `file_print.print_JFH`. Standardize to `np.array` everywhere, update consumers accordingly. -- Rotations and DCMs passed as nested Python lists intermixed with arrays; should be `np.ndarray` consistently. - -4) Error handling/logging gaps -- Empty try/except that silently sets None or returns (e.g., many config loads) with no logs. This makes debugging difficult in production runs. -- Many commented-out print statements, but no structured logging. Prefer logging library. -- File and directory writes don’t catch filesystem errors; return values/status are unused. - -5) Hard-coded paths and relative resources -- Some methods refer to `../stl/...` instead of case-dir bound paths (e.g., `VisitingVehicle.initiate_plume_mesh` uses `../data/stl/mold_funnel.stl`), while others correctly use `self.case_dir`. This inconsistency will break when run from different cwd. - -6) Configuration and “stateful object” coupling -- Many classes re-read `config.ini` individually; `MissionEnvironment` could be the single source. -- `LogisticsModule` inherits from `VisitingVehicle` but duplicates some constructor/config reading; `Vehicle` also reads config independently. Consolidate source of truth for config and case_dir in `MissionEnvironment` and pass references. - -7) API clarity -- Some methods return different shapes conditionally (e.g., `set_strike_fields` returns different tuples based on kinetics mode), which complicates usage. -- `RPOD.jfh_plume_strikes` returns `firing_data` but also writes VTK files and updates internal state — consider a clearer separation of compute vs side-effects. - -8) Testability and separation of concerns -- Many methods do heavy I/O (read/write files, create directories) in the same code paths as computations, making unit testing hard. -- Compute functions should accept inputs and return data; persistence and visualization should be separate adapters. - -9) Style/naming consistency -- CamelCase and snake_case mixed for file/class/method names in places. -- Some typos: “Caculated”, “inlcude”, “instatiated”, etc. Minor but present. - -## Concrete examples of duplication and opportunities - -- rotation_matrix_from_vectors - - Found in both `RPOD.py` and `JetFiringHistory.py`. Centralize in utils; update imports. -- Nearly identical loops in `graph_jfh` and `visualize_sweep` - - Construct VV mesh, thrusters, clusters, rotate/translate, aggregate, save. -- `calc_trans_performance` in two places - - Same variables and math in `six_dof_dynamics.py` and `flight_eval.py`. - -## Phased refactoring plan - -### Phase 0: Safety net and decisions (short) -- Add a lightweight logging setup (logging.getLogger("pyrpod")) and start replacing print with logger.debug/info/warning. -- Decide on numpy array standard for rotations (drop np.matrix); encode in a short style guide doc in `pyrpod/README_new.md`. -- Add a tiny runtime guard for case_dir existence and helpful error if missing. - -### Phase 1: DRY quick wins (low risk, local changes) -- Extract `rotation_matrix_from_vectors` to `pyrpod/util/math/transform.py` and update imports in `rpod/RPOD.py` and `rpod/JetFiringHistory.py`. -- Factor out a small filesystem helper: `pyrpod/util/fs.py` with `ensure_dir(path)`, `ensure_parent_dir(file_path)`. -- Deduplicate `calc_trans_performance`: keep once in `SixDOFDynamics` and have `FlightEvaluator` call into it (or import a mission.util). -- Standardize to `np.array`: - - Replace uses of `np.matrix` in `VisitingVehicle.transform_plume_mesh`, `JetFiringHistory`, and RPOD rotation construction; adjust downstream code that depends on `.A`. - - Update `util/io/file_print.py` to handle arrays using `rot[i][j][k]` safely. -- Fix hard-coded relative paths: - - Replace `../data/stl/...` and `../stl/...` with `self.case_dir + 'stl/...'` consistently. - -Acceptance for Phase 1: -- Unit tests still pass (including `tests/rpod/rpod_unit_test_03.py` which uses print_JFH). -- No functional change in outputs except path normalization and stable logs. -- No use of np.matrix remains. - -### Phase 2: Separate compute from I/O/visualization (medium) -- Split `rpod/RPOD.py` into: - - `rpod/geometry.py` (mesh building: VV/cluster/thruster plume composition, transform pipelines) - - `rpod/impingement.py` (plume strikes computation, returns arrays without writing) - - `rpod/io.py` (VTK/STL writers and result directory setup) - - `rpod/one_d_approach.py` (1D kinematics and mass calc, JFH generation utilities) - - Keep a thin `RPOD` orchestrator that wires these pieces. -- In `MissionPlanner` and `MissionEnvironment`, centralize config and case_dir; stop re-reading configs in Vehicle classes. Pass environment to components that need it. -- Make `MissionEnvironment` the canonical provider of `vv` and `tv`; remove duplicated config readers in `Vehicle` so they accept an environment or config object in constructor. - -Acceptance for Phase 2: -- RPOD flows still run for current cases (existing case dirs). -- Graph generation and STL/VTK export still produced with same filenames. -- Compute functions can be called without touching filesystem (add 1-2 new unit tests around pure functions). - -### Phase 3: API cleanups and stronger typing (medium) -- Ensure consistent return types/signatures: - - `set_strike_fields` and `set_plume_strike_fields` return consistent namedtuple/dataclass (e.g., StrikeFields with optional physics arrays set to None if not used). -- Introduce dataclasses for common structures: - - Thruster, Cluster, FiringEvent (replace raw dicts with typed fields). - - This improves readability and reduces key-typo bugs. -- Simplify thruster grouping config parsing: return validated structures, add small schema validation/errors with helpful messages. - -Acceptance for Phase 3: -- No breaking changes to external behavior; internal code compiles and runs with new types. -- Minimal adapters added where dicts were expected. - -### Phase 4: Testing and docs (ongoing) -- Add unit tests for: - - `rotation_matrix_from_vectors` (edge case: identical vectors) - - Plume transformation pipeline ordering - - `calc_trans_performance` with simple inputs -- Add docstrings and a CONTRIBUTING.md style snippet on arrays vs matrices, path handling, and config expectations. - -### Phase 5: Optional larger improvements -- Consider a “ResultsWriter” abstraction to capture VTK, CSV, images without littering code paths. -- Consider serialization of intermediate computed results to enable post-processing without re-running simulation. -- Explore using a geometry/transform helper library or unify with a small internal matrix utility wrapping numpy. - -## Specific, actionable items list (mapping to code) -- Create `pyrpod/util/math/transform.py` and move `rotation_matrix_from_vectors`. -- Create `pyrpod/util/fs.py` with `ensure_dir(path)` and `ensure_parent_dir(file_path)`. -- Replace: - - All os.path.isdir/os.mkdir blocks with ensure_dir. - - All `np.matrix` usage with `np.array`. -- Merge `calc_trans_performance` into one location; import and reuse in `FlightEvaluator`. -- Normalize all STL path references to `self.environment.case_dir`-anchored paths. -- In `file_print.py`, remove reliance on `.A` and assume `rot` is ndarray. -- In `MissionEnvironment`, ensure single config read and pass environment into `VisitingVehicle` and `TargetVehicle` so they don’t re-read `config.ini`. - -## Positive impact -- Less duplication, easier to update math/transform once. -- Clearer module responsibilities, smaller files, easier testing. -- Consistent path handling and logging make runs more robust in different environments. -- Moving to np.array reduces surprises and aligns with modern NumPy usage. - -## Risks and mitigations -- Changing from np.matrix to np.array can break code expecting `.A` or 2D coercion. Mitigation: change all sites in one sweep and add small tests. -- Centralizing config in `MissionEnvironment` requires touching constructors; do it compatibly (allow passing environment or case_dir, default to current behavior initially). -- Splitting RPOD will require updating imports. Do it incrementally with shims (keep old class but delegate to new modules internally), then clean up. - -## Closing summary -- The codebase is neatly organized by domain and already has decent docstrings. -- The main opportunities are consolidating repeated math/IO snippets, modernizing arrays, and separating compute from IO-heavy orchestration. -- The phased plan starts with low-risk DRY fixes and logging, then moves toward clearer APIs and structure without changing external behavior. +# PyRPOD `pyrpod/` Code Review — DRY and Maintainability + +Date: 2025-09-18 +Scope: All Python under `pyrpod/` (mission, rpod, vehicle, mdao, orbital, plume, util) + +## What I looked at +- Packages: mission, rpod, vehicle, plume, mdao, orbital, util +- Representative files: + - `mission/`: `MissionPlanner.py`, `six_dof_dynamics.py`, `flight_eval.py`, `MissionEnvironment.py` + - `rpod/`: `RPOD.py`, `JetFiringHistory.py` + - `vehicle/`: `Vehicle.py`, `VisitingVehicle.py`, `LogisticsModule.py` + - `mdao/`: `SweepConfig.py` + - `plume/`: `RarefiedPlumeGasKinetics.py`, `IsentropicExpansion.py` + - `util/`: `io/file_print.py`, `stl/transform_stl.py` + +## Highlights (what’s working well) +- Clear domain structure: mission, rpod, vehicle, plume, mdao are separated logically. +- Good docstrings throughout, with parameter/return descriptions that help future readers. +- Config-driven design via `configparser` keeps cases flexible without code changes. +- Useful utilities: + - JFH I/O helpers in `util/io/file_print.py` with tests referring to them. + - `Vehicle.convert_stl_to_vtk*` centralizes mesh-to-VTK conversion. +- Physics/maths organization: + - Plume models in `plume/` are self-contained and reasonably documented. + - Orbital calculations are isolated in `orbital/HohmannTransfer.py`. +- Encapsulation: + - `MissionEnvironment` wraps case setup (vehicles, JFH) and offers logging slots for results. + - `MissionPlanner` composes submodules and suggests a clean orchestration point. + +## DRY and maintainability concerns + +1) Duplicated helpers and logic +- rotation_matrix_from_vectors duplicated + - Defined in `rpod/RPOD.py` and `rpod/JetFiringHistory.py`, and used in many places. This belongs in a single utility (e.g., `pyrpod/util/math/transform.py`). +- Near-identical STL/mesh routines + - Similar logic in `RPOD.graph_jfh` and `RPOD.visualize_sweep` (build VV mesh, rotate/translate, build plume meshes, optional clusters, save STL). This can be a single function with options. + - Multiple places create results directories and save assets using repeated os.path checks; could be factored into a small FS helper. +- Repeated “plume transformation pipeline” + - Sequence: DCM for thruster, rotate plume, rotate with VV orientation, translate by VV position, then cluster offset, then thruster exit. This appears in multiple methods; factor into a utility. +- Trans/rot performance calculations repeated + - `mission/six_dof_dynamics.py` and `mission/flight_eval.py` both have `calc_trans_performance` with identical logic. Consolidate into dynamics or a helper. + +2) Mixed responsibilities and large classes +- `rpod/RPOD.py` is very large and mixes: + - Visualization (matplotlib creation, saving images) + - STL composition and file I/O + - Physics (1D approach, delta-v computation, time multiplier) + - Plume strikes computation and export + - This should be split into smaller services/modules. As-is, it’s hard to test and evolve. + +3) Inconsistent data types and old NumPy patterns +- Use of `np.matrix` (discouraged) vs `np.array`. Mixing `np.matrix` vs `np.array` with `.A` indexing in `file_print.print_JFH`. Standardize to `np.array` everywhere, update consumers accordingly. +- Rotations and DCMs passed as nested Python lists intermixed with arrays; should be `np.ndarray` consistently. + +4) Error handling/logging gaps +- Empty try/except that silently sets None or returns (e.g., many config loads) with no logs. This makes debugging difficult in production runs. +- Many commented-out print statements, but no structured logging. Prefer logging library. +- File and directory writes don’t catch filesystem errors; return values/status are unused. + +5) Hard-coded paths and relative resources +- Some methods refer to `../stl/...` instead of case-dir bound paths (e.g., `VisitingVehicle.initiate_plume_mesh` uses `../data/stl/mold_funnel.stl`), while others correctly use `self.case_dir`. This inconsistency will break when run from different cwd. + +6) Configuration and “stateful object” coupling +- Many classes re-read `config.ini` individually; `MissionEnvironment` could be the single source. +- `LogisticsModule` inherits from `VisitingVehicle` but duplicates some constructor/config reading; `Vehicle` also reads config independently. Consolidate source of truth for config and case_dir in `MissionEnvironment` and pass references. + +7) API clarity +- Some methods return different shapes conditionally (e.g., `set_strike_fields` returns different tuples based on kinetics mode), which complicates usage. +- `RPOD.jfh_plume_strikes` returns `firing_data` but also writes VTK files and updates internal state — consider a clearer separation of compute vs side-effects. + +8) Testability and separation of concerns +- Many methods do heavy I/O (read/write files, create directories) in the same code paths as computations, making unit testing hard. +- Compute functions should accept inputs and return data; persistence and visualization should be separate adapters. + +9) Style/naming consistency +- CamelCase and snake_case mixed for file/class/method names in places. +- Some typos: “Caculated”, “inlcude”, “instatiated”, etc. Minor but present. + +## Concrete examples of duplication and opportunities + +- rotation_matrix_from_vectors + - Found in both `RPOD.py` and `JetFiringHistory.py`. Centralize in utils; update imports. +- Nearly identical loops in `graph_jfh` and `visualize_sweep` + - Construct VV mesh, thrusters, clusters, rotate/translate, aggregate, save. +- `calc_trans_performance` in two places + - Same variables and math in `six_dof_dynamics.py` and `flight_eval.py`. + +## Phased refactoring plan + +### Phase 0: Safety net and decisions (short) +- Add a lightweight logging setup (logging.getLogger("pyrpod")) and start replacing print with logger.debug/info/warning. +- Decide on numpy array standard for rotations (drop np.matrix); encode in a short style guide doc in `pyrpod/README_new.md`. +- Add a tiny runtime guard for case_dir existence and helpful error if missing. + +### Phase 1: DRY quick wins (low risk, local changes) +- Extract `rotation_matrix_from_vectors` to `pyrpod/util/math/transform.py` and update imports in `rpod/RPOD.py` and `rpod/JetFiringHistory.py`. +- Factor out a small filesystem helper: `pyrpod/util/fs.py` with `ensure_dir(path)`, `ensure_parent_dir(file_path)`. +- Deduplicate `calc_trans_performance`: keep once in `SixDOFDynamics` and have `FlightEvaluator` call into it (or import a mission.util). +- Standardize to `np.array`: + - Replace uses of `np.matrix` in `VisitingVehicle.transform_plume_mesh`, `JetFiringHistory`, and RPOD rotation construction; adjust downstream code that depends on `.A`. + - Update `util/io/file_print.py` to handle arrays using `rot[i][j][k]` safely. +- Fix hard-coded relative paths: + - Replace `../data/stl/...` and `../stl/...` with `self.case_dir + 'stl/...'` consistently. + +Acceptance for Phase 1: +- Unit tests still pass (including `tests/rpod/rpod_unit_test_03.py` which uses print_JFH). +- No functional change in outputs except path normalization and stable logs. +- No use of np.matrix remains. + +### Phase 2: Separate compute from I/O/visualization (medium) +- Split `rpod/RPOD.py` into: + - `rpod/geometry.py` (mesh building: VV/cluster/thruster plume composition, transform pipelines) + - `rpod/impingement.py` (plume strikes computation, returns arrays without writing) + - `rpod/io.py` (VTK/STL writers and result directory setup) + - `rpod/one_d_approach.py` (1D kinematics and mass calc, JFH generation utilities) + - Keep a thin `RPOD` orchestrator that wires these pieces. +- In `MissionPlanner` and `MissionEnvironment`, centralize config and case_dir; stop re-reading configs in Vehicle classes. Pass environment to components that need it. +- Make `MissionEnvironment` the canonical provider of `vv` and `tv`; remove duplicated config readers in `Vehicle` so they accept an environment or config object in constructor. + +Acceptance for Phase 2: +- RPOD flows still run for current cases (existing case dirs). +- Graph generation and STL/VTK export still produced with same filenames. +- Compute functions can be called without touching filesystem (add 1-2 new unit tests around pure functions). + +### Phase 3: API cleanups and stronger typing (medium) +- Ensure consistent return types/signatures: + - `set_strike_fields` and `set_plume_strike_fields` return consistent namedtuple/dataclass (e.g., StrikeFields with optional physics arrays set to None if not used). +- Introduce dataclasses for common structures: + - Thruster, Cluster, FiringEvent (replace raw dicts with typed fields). + - This improves readability and reduces key-typo bugs. +- Simplify thruster grouping config parsing: return validated structures, add small schema validation/errors with helpful messages. + +Acceptance for Phase 3: +- No breaking changes to external behavior; internal code compiles and runs with new types. +- Minimal adapters added where dicts were expected. + +### Phase 4: Testing and docs (ongoing) +- Add unit tests for: + - `rotation_matrix_from_vectors` (edge case: identical vectors) + - Plume transformation pipeline ordering + - `calc_trans_performance` with simple inputs +- Add docstrings and a CONTRIBUTING.md style snippet on arrays vs matrices, path handling, and config expectations. + +### Phase 5: Optional larger improvements +- Consider a “ResultsWriter” abstraction to capture VTK, CSV, images without littering code paths. +- Consider serialization of intermediate computed results to enable post-processing without re-running simulation. +- Explore using a geometry/transform helper library or unify with a small internal matrix utility wrapping numpy. + +## Specific, actionable items list (mapping to code) +- Create `pyrpod/util/math/transform.py` and move `rotation_matrix_from_vectors`. +- Create `pyrpod/util/fs.py` with `ensure_dir(path)` and `ensure_parent_dir(file_path)`. +- Replace: + - All os.path.isdir/os.mkdir blocks with ensure_dir. + - All `np.matrix` usage with `np.array`. +- Merge `calc_trans_performance` into one location; import and reuse in `FlightEvaluator`. +- Normalize all STL path references to `self.environment.case_dir`-anchored paths. +- In `file_print.py`, remove reliance on `.A` and assume `rot` is ndarray. +- In `MissionEnvironment`, ensure single config read and pass environment into `VisitingVehicle` and `TargetVehicle` so they don’t re-read `config.ini`. + +## Positive impact +- Less duplication, easier to update math/transform once. +- Clearer module responsibilities, smaller files, easier testing. +- Consistent path handling and logging make runs more robust in different environments. +- Moving to np.array reduces surprises and aligns with modern NumPy usage. + +## Risks and mitigations +- Changing from np.matrix to np.array can break code expecting `.A` or 2D coercion. Mitigation: change all sites in one sweep and add small tests. +- Centralizing config in `MissionEnvironment` requires touching constructors; do it compatibly (allow passing environment or case_dir, default to current behavior initially). +- Splitting RPOD will require updating imports. Do it incrementally with shims (keep old class but delegate to new modules internally), then clean up. + +## Closing summary +- The codebase is neatly organized by domain and already has decent docstrings. +- The main opportunities are consolidating repeated math/IO snippets, modernizing arrays, and separating compute from IO-heavy orchestration. +- The phased plan starts with low-risk DRY fixes and logging, then moves toward clearer APIs and structure without changing external behavior. diff --git a/pyproject.toml b/pyproject.toml index 7867429..a20d6eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,41 +1,41 @@ -[tool.black] -line-length = 88 -target-version = ["py311"] -skip-string-normalization = false - -[tool.ruff] -line-length = 88 -extend-ignore = ["E203", "W503"] -# recommended rule groups; adjust to taste -select = ["E", "F", "W", "C90", "I", "B"] - -[tool.isort] -profile = "black" -line_length = 88 - -[tool.mypy] -python_version = "3.11" -strict = false -ignore_missing_imports = true -show_error_codes = true - -[tool.ruff.per-file-ignores] -# Ignore some noisy rules in tests and generated files -"tests/**" = ["D", "S101"] -"**/__pycache__/**" = ["ALL"] -"venv-pyrpod/**" = ["ALL"] - -[tool.pytest.ini_options] -testpaths = ["tests"] -# Legacy test files are named "__test_NN.py" rather than pytest's default patterns. -python_files = ["test_*.py", "*_test_*.py"] -pythonpath = ["."] -markers = [ - "unit: unit-level test cases", - "integration: integration test cases", - "verification: verification/validation test cases", - "mdao: mdao subsystem tests", - "mission: mission subsystem tests", - "plume: plume subsystem tests", - "rpod: rpod subsystem tests", -] +[tool.black] +line-length = 88 +target-version = ["py311"] +skip-string-normalization = false + +[tool.ruff] +line-length = 88 +extend-ignore = ["E203", "W503"] +# recommended rule groups; adjust to taste +select = ["E", "F", "W", "C90", "I", "B"] + +[tool.isort] +profile = "black" +line_length = 88 + +[tool.mypy] +python_version = "3.11" +strict = false +ignore_missing_imports = true +show_error_codes = true + +[tool.ruff.per-file-ignores] +# Ignore some noisy rules in tests and generated files +"tests/**" = ["D", "S101"] +"**/__pycache__/**" = ["ALL"] +"venv-pyrpod/**" = ["ALL"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +# Legacy test files are named "__test_NN.py" rather than pytest's default patterns. +python_files = ["test_*.py", "*_test_*.py"] +pythonpath = ["."] +markers = [ + "unit: unit-level test cases", + "integration: integration test cases", + "verification: verification/validation test cases", + "mdao: mdao subsystem tests", + "mission: mission subsystem tests", + "plume: plume subsystem tests", + "rpod: rpod subsystem tests", +] diff --git a/pyrpod/config_test.py b/pyrpod/config_test.py index 77fcbfb..12de6ac 100644 --- a/pyrpod/config_test.py +++ b/pyrpod/config_test.py @@ -1,24 +1,24 @@ -# This is simply to get a minumum functionality going. -# Developing a better soltuion for handling RCS working groups is a top priority for PyRPOD. - -import configparser -config = configparser.ConfigParser() - -config['thruster_groups'] = { - '+x': ['P1T2', 'P2T2', 'P3T2', 'P4T2', 'P5T2', 'P6T2', 'P7T2', 'P8T2'], - '-x': ['P1T1', 'P2T1', 'P3T1', 'P4T1', 'P5T1', 'P6T1', 'P7T1', 'P8T1'], - '+y': ['P1T3', 'P2T3', 'P3T4', 'P4T3', 'P5T3', 'P6T3', 'P7T4', 'P8T3'], - '-y': ['P1T4', 'P2T4', 'P3T3', 'P4T4', 'P5T4', 'P6T4', 'P7T3', 'P8T4'], - '+z': ['P1T4', 'P2T3', 'P3T3', 'P4T3', 'P5T4', 'P6T3', 'P7T3', 'P8T3'], - '-z': ['P1T3', 'P2T4', 'P3T4', 'P4T4', 'P5T3', 'P6T4', 'P7T4', 'P8T4'], - - '+roll': ['P1T4', 'P2T4', 'P3T4', 'P4T3', 'P5T4', 'P6T4', 'P7T4', 'P8T3'], - '-roll': ['P1T3', 'P2T3', 'P3T3', 'P4T4', 'P5T3', 'P6T3', 'P7T3', 'P8T4'], - '+pitch': ['P1T1', 'P2T1', 'P7T2', 'P8T2'], - '-pitch': ['P3T1', 'P4T1', 'P5T2', 'P6T2'], - '+yaw': ['P1T4', 'P4T4', 'P6T3', 'P7T4'], - '-yaw': ['P2T3', 'P3T4', 'P5T4', 'P8T4'] - } - -with open('example.ini', 'w') as configfile: +# This is simply to get a minumum functionality going. +# Developing a better soltuion for handling RCS working groups is a top priority for PyRPOD. + +import configparser +config = configparser.ConfigParser() + +config['thruster_groups'] = { + '+x': ['P1T2', 'P2T2', 'P3T2', 'P4T2', 'P5T2', 'P6T2', 'P7T2', 'P8T2'], + '-x': ['P1T1', 'P2T1', 'P3T1', 'P4T1', 'P5T1', 'P6T1', 'P7T1', 'P8T1'], + '+y': ['P1T3', 'P2T3', 'P3T4', 'P4T3', 'P5T3', 'P6T3', 'P7T4', 'P8T3'], + '-y': ['P1T4', 'P2T4', 'P3T3', 'P4T4', 'P5T4', 'P6T4', 'P7T3', 'P8T4'], + '+z': ['P1T4', 'P2T3', 'P3T3', 'P4T3', 'P5T4', 'P6T3', 'P7T3', 'P8T3'], + '-z': ['P1T3', 'P2T4', 'P3T4', 'P4T4', 'P5T3', 'P6T4', 'P7T4', 'P8T4'], + + '+roll': ['P1T4', 'P2T4', 'P3T4', 'P4T3', 'P5T4', 'P6T4', 'P7T4', 'P8T3'], + '-roll': ['P1T3', 'P2T3', 'P3T3', 'P4T4', 'P5T3', 'P6T3', 'P7T3', 'P8T4'], + '+pitch': ['P1T1', 'P2T1', 'P7T2', 'P8T2'], + '-pitch': ['P3T1', 'P4T1', 'P5T2', 'P6T2'], + '+yaw': ['P1T4', 'P4T4', 'P6T3', 'P7T4'], + '-yaw': ['P2T3', 'P3T4', 'P5T4', 'P8T4'] + } + +with open('example.ini', 'w') as configfile: config.write(configfile) \ No newline at end of file diff --git a/pyrpod/logging_utils.py b/pyrpod/logging_utils.py index df417be..12a68d1 100644 --- a/pyrpod/logging_utils.py +++ b/pyrpod/logging_utils.py @@ -1,29 +1,29 @@ -import logging -import os - - -def get_logger(name: str = "pyrpod", level: str | None = None) -> logging.Logger: - """ - Return a configured logger for the PyRPOD project. - - - Uses a StreamHandler with a concise formatter - - Respects PYRPOD_LOG_LEVEL and PYRPOD_LOG_FORMAT env vars - - Avoids duplicate handlers on repeated calls - """ - logger = logging.getLogger(name) - if not logger.handlers: - handler = logging.StreamHandler() - fmt = os.environ.get( - "PYRPOD_LOG_FORMAT", - "%(asctime)s [%(levelname)s] %(name)s: %(message)s", - ) - handler.setFormatter(logging.Formatter(fmt)) - logger.addHandler(handler) - - level_name = (level or os.environ.get("PYRPOD_LOG_LEVEL", "INFO")).upper() - logger.setLevel(getattr(logging, level_name, logging.INFO)) - - # Prevent messages from bubbling to the root logger if user configured it - logger.propagate = False - - return logger +import logging +import os + + +def get_logger(name: str = "pyrpod", level: str | None = None) -> logging.Logger: + """ + Return a configured logger for the PyRPOD project. + + - Uses a StreamHandler with a concise formatter + - Respects PYRPOD_LOG_LEVEL and PYRPOD_LOG_FORMAT env vars + - Avoids duplicate handlers on repeated calls + """ + logger = logging.getLogger(name) + if not logger.handlers: + handler = logging.StreamHandler() + fmt = os.environ.get( + "PYRPOD_LOG_FORMAT", + "%(asctime)s [%(levelname)s] %(name)s: %(message)s", + ) + handler.setFormatter(logging.Formatter(fmt)) + logger.addHandler(handler) + + level_name = (level or os.environ.get("PYRPOD_LOG_LEVEL", "INFO")).upper() + logger.setLevel(getattr(logging, level_name, logging.INFO)) + + # Prevent messages from bubbling to the root logger if user configured it + logger.propagate = False + + return logger diff --git a/pyrpod/mdao/TradeStudy.py b/pyrpod/mdao/TradeStudy.py index f265b5e..baf92bb 100644 --- a/pyrpod/mdao/TradeStudy.py +++ b/pyrpod/mdao/TradeStudy.py @@ -1,290 +1,290 @@ -import os -import csv -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mdao import SweepConfig -import configparser - -class TradeStudy(): - def __init__(self, case_dir): - self.case_dir = case_dir - config = configparser.ConfigParser() - config.read(self.case_dir + "config.ini") - self.config = config - - def init_trade_study(self, lm, tv): - """ - Organizes data needed to kick off an RPOD trade study. - - Mainly done by properly configuring an RPOD object - - """ - # Save variable name for readability. - case_dir = self.case_dir - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - - # Instantiate RPOD object. - rpod = PlumeStrikeEstimationStudy.RPOD(case_dir) - rpod.study_init(jfh, tv, lm) - self.rpod = rpod - - def init_trade_study_case(self): - """ - Resets JFH data according to current case key. - - Case key is a unique identified for a specific configuration within the trade study. - """ - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(self.case_dir) - jfh.config['jfh']['jfh'] = self.rpod.get_case_key() + ".A" - jfh.read_jfh() - - self.rpod.jfh = jfh - - def print_mission_report(self): - - case_key = self.rpod.get_case_key() - - max_v0 = self.max_v0 - - fuel_mass = self.rpod.fuel_mass - - max_pressure = self.rpod.max_pressure - max_shear = self.rpod.max_shear - max_heat_rate = self.rpod.max_heat_rate - max_heat_load = self.rpod.max_heat_load - max_cum_heat_load = self.rpod.max_cum_heat_load - - # Check if the file exists - report_path = self.case_dir + 'results/MissionReport.csv' - file_exists = os.path.isfile(report_path) - - # Open CSV file in append mode - with open(report_path, 'a', newline='') as csvfile: - csv_writer = csv.writer(csvfile) - - # Write header if the file is newly created - if not file_exists: - csv_writer.writerow(['CaseKey', 'MaxV0', 'FuelMass', 'MaxPressure', 'MaxShear', 'MaxHeatRate', 'MaxHeatLoad', 'MaxCumulativeHeatLoad']) - - # Write data row - csv_writer.writerow([ - case_key, - max_v0, - fuel_mass, - max_pressure, - max_shear, - max_heat_rate, - max_heat_load, - max_cum_heat_load - ]) - - def graph_mission_report(self, report_results): - """ - """ - # Extract the first column (assuming it's the parameter you want to plot against) - first_column = report_results.columns[0] - # Convert DataFrame columns to NumPy arrays for indexing - x_values = report_results[first_column].to_numpy() - print(report_results) - # Plot each parameter against the first parameter - for column in report_results.columns[1:8]: - y_values = report_results[column].to_numpy() - plt.figure() # Create a new figure for each plot - plt.plot(x_values,y_values, label=column) - plt.xlabel(first_column) - plt.ylabel(column) - plt.title(f'{column} vs {first_column}') - plt.legend() - plt.grid(True) - - # Show all plots - plt.show() - - - def interpret_mission_report(self): - """ - """ - report_path = self.case_dir + 'results/MissionReport.csv' - report_results = pd.read_csv(report_path) - - max_pressure = float(self.rpod.config['tv']['normal_pressure']) - max_shear = float(self.rpod.config['tv']['shear_pressure']) - max_heat_rate = float(self.rpod.config['tv']['heat_flux']) - # max_heat_load = float(self.rpod.config['tv'][]) no such constraint - max_cum_heat_load = float(self.rpod.config['tv']['heat_flux_load']) - - plume_status = [] - plume_failure_mode = [] - - for i, row in report_results.iterrows(): - if row['MaxPressure'] > max_pressure: - plume_status.append('fail') - plume_failure_mode.append('pressure') - elif row['MaxShear'] > max_shear: - plume_status.append('fail') - plume_failure_mode.append('shear') - elif row['MaxHeatRate'] > max_heat_rate: - plume_status.append('fail') - plume_failure_mode.append('heat_flux') - # elif report_results['MaxHeatLoad'] > max_heat_load: no such constraint - # plume_status.append('fail') - elif row['MaxCumulativeHeatLoad'] > max_cum_heat_load: - plume_status.append('fail') - plume_failure_mode.append('cumulative_heat_flux_load') - else: - plume_status.append('pass') - plume_failure_mode.append('none') - - report_results['PlumeStatus'] = plume_status - report_results['PlumeFailureMode'] = plume_failure_mode - self.graph_mission_report(report_results) - - def run_axial_overshoot_sweep(self, sweep_vars, lm, tv): - """ - Simple variable sweep study that assesses RCS performance for a given set of axial overshoot velocity values. - """ - - # Organize variables to sweet over. - axial_overshoot = sweep_vars['axial_overshoot'] - self.max_v0 = np.max(axial_overshoot) - - # Link elements for RPOD analysis. - self.init_trade_study(lm, tv) - - # Create results directory if necessary. - results_dir = self.case_dir + 'results' - if not os.path.isdir(results_dir): - os.mkdir(results_dir) - - # Loop through over shoot velocities to test. - for i, v_o in enumerate(axial_overshoot): - - # print(i, v_o) - # # Set unique case identifier within trade study. - self.rpod.set_case_key(i, 0) - - # Create JFH for a given velocity. - self.rpod.print_jfh_1d_approach_n_fire( - tv.v_ida, - v_o, - tv.r_o, - n_firings = 100, - trade_study = True - ) - - # Reset JFH according to specific case. - self.init_trade_study_case() - self.rpod.graph_jfh(trade_study= True) - self.rpod.jfh_plume_strikes(trade_study = True) - - self.print_mission_report() - self.interpret_mission_report() - - - # self.rpod.jfh_plume_strikes(trade_study = True) - - def run_surface_cant_sweep(self, sweep_vars, lm, tv): - """ - """ - - # Organize variables to sweet over. - surface_cant_angles = sweep_vars['surface_cant_angles'] - v_o = sweep_vars['axial_overshoot'] - self.max_v0 = np.max(v_o) - - # Link elements for RPOD analysis. - self.init_trade_study(lm, tv) - - angle_sweep = SweepConfig.SweepDecelAngles(lm.thruster_data, lm.rcs_groups) - - # Create results directory if necessary. - results_dir = self.case_dir + 'results' - if not os.path.isdir(results_dir): - os.mkdir(results_dir) - - # Loop through over shoot velocities to test. - for i, cant in enumerate(surface_cant_angles): - - cant = np.deg2rad(cant) - lm.decel_cant = cant - - new_tcd = angle_sweep.cant_decel_thrusters(cant) - lm.set_thruster_config(new_tcd) - - # print(i, v_o) - # # Set unique case identifier within trade study. - self.rpod.set_case_key(0, i) - - # Create JFH for a given velocity. - self.rpod.print_jfh_1d_approach_n_fire( - tv.v_ida, - v_o, - tv.r_o, - n_firings = 100, - trade_study = True - ) - - # Reset JFH according to specific case. - self.init_trade_study_case() - - self.rpod.graph_jfh(trade_study= True) - self.rpod.jfh_plume_strikes(trade_study = True) - - self.print_mission_report() - self.interpret_mission_report() - - def run_multi_var_sweep(self, sweep_vars, lm, tv): - """ - """ - # Organize variables to sweet over. - axial_overshoot = sweep_vars['axial_overshoot'] - surface_cant_angles = sweep_vars['surface_cant_angles'] - self.max_v0 = np.max(axial_overshoot) - - # Link elements for RPOD analysis. - self.init_trade_study(lm, tv) - - angle_sweep = SweepConfig.SweepDecelAngles(lm.thruster_data, lm.rcs_groups) - - # Create results directory if necessary. - results_dir = self.case_dir + 'results' - if not os.path.isdir(results_dir): - os.mkdir(results_dir) - - # Loop through over shoot velocities to test. - for i, v_o in enumerate(axial_overshoot): - for j, cant in enumerate(surface_cant_angles): - - lm.decel_cant = cant - - new_tcd = angle_sweep.cant_decel_thrusters(cant) - lm.set_thruster_config(new_tcd) - - # print(i, v_o) - # # Set unique case identifier within trade study. - self.rpod.set_case_key(i, j) - - # Create JFH for a given velocity. - self.rpod.print_jfh_1d_approach_n_fire( - tv.v_ida, - v_o, - tv.r_o, - n_firings = 10, - trade_study = True - ) - - # Reset JFH according to specific case. - self.init_trade_study_case() - - self.rpod.graph_jfh(trade_study= True) - self.rpod.jfh_plume_strikes(trade_study = True) - - self.print_mission_report() +import os +import csv +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mdao import SweepConfig +import configparser + +class TradeStudy(): + def __init__(self, case_dir): + self.case_dir = case_dir + config = configparser.ConfigParser() + config.read(self.case_dir + "config.ini") + self.config = config + + def init_trade_study(self, lm, tv): + """ + Organizes data needed to kick off an RPOD trade study. + + Mainly done by properly configuring an RPOD object + + """ + # Save variable name for readability. + case_dir = self.case_dir + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + + # Instantiate RPOD object. + rpod = PlumeStrikeEstimationStudy.RPOD(case_dir) + rpod.study_init(jfh, tv, lm) + self.rpod = rpod + + def init_trade_study_case(self): + """ + Resets JFH data according to current case key. + + Case key is a unique identified for a specific configuration within the trade study. + """ + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(self.case_dir) + jfh.config['jfh']['jfh'] = self.rpod.get_case_key() + ".A" + jfh.read_jfh() + + self.rpod.jfh = jfh + + def print_mission_report(self): + + case_key = self.rpod.get_case_key() + + max_v0 = self.max_v0 + + fuel_mass = self.rpod.fuel_mass + + max_pressure = self.rpod.max_pressure + max_shear = self.rpod.max_shear + max_heat_rate = self.rpod.max_heat_rate + max_heat_load = self.rpod.max_heat_load + max_cum_heat_load = self.rpod.max_cum_heat_load + + # Check if the file exists + report_path = self.case_dir + 'results/MissionReport.csv' + file_exists = os.path.isfile(report_path) + + # Open CSV file in append mode + with open(report_path, 'a', newline='') as csvfile: + csv_writer = csv.writer(csvfile) + + # Write header if the file is newly created + if not file_exists: + csv_writer.writerow(['CaseKey', 'MaxV0', 'FuelMass', 'MaxPressure', 'MaxShear', 'MaxHeatRate', 'MaxHeatLoad', 'MaxCumulativeHeatLoad']) + + # Write data row + csv_writer.writerow([ + case_key, + max_v0, + fuel_mass, + max_pressure, + max_shear, + max_heat_rate, + max_heat_load, + max_cum_heat_load + ]) + + def graph_mission_report(self, report_results): + """ + """ + # Extract the first column (assuming it's the parameter you want to plot against) + first_column = report_results.columns[0] + # Convert DataFrame columns to NumPy arrays for indexing + x_values = report_results[first_column].to_numpy() + print(report_results) + # Plot each parameter against the first parameter + for column in report_results.columns[1:8]: + y_values = report_results[column].to_numpy() + plt.figure() # Create a new figure for each plot + plt.plot(x_values,y_values, label=column) + plt.xlabel(first_column) + plt.ylabel(column) + plt.title(f'{column} vs {first_column}') + plt.legend() + plt.grid(True) + + # Show all plots + plt.show() + + + def interpret_mission_report(self): + """ + """ + report_path = self.case_dir + 'results/MissionReport.csv' + report_results = pd.read_csv(report_path) + + max_pressure = float(self.rpod.config['tv']['normal_pressure']) + max_shear = float(self.rpod.config['tv']['shear_pressure']) + max_heat_rate = float(self.rpod.config['tv']['heat_flux']) + # max_heat_load = float(self.rpod.config['tv'][]) no such constraint + max_cum_heat_load = float(self.rpod.config['tv']['heat_flux_load']) + + plume_status = [] + plume_failure_mode = [] + + for i, row in report_results.iterrows(): + if row['MaxPressure'] > max_pressure: + plume_status.append('fail') + plume_failure_mode.append('pressure') + elif row['MaxShear'] > max_shear: + plume_status.append('fail') + plume_failure_mode.append('shear') + elif row['MaxHeatRate'] > max_heat_rate: + plume_status.append('fail') + plume_failure_mode.append('heat_flux') + # elif report_results['MaxHeatLoad'] > max_heat_load: no such constraint + # plume_status.append('fail') + elif row['MaxCumulativeHeatLoad'] > max_cum_heat_load: + plume_status.append('fail') + plume_failure_mode.append('cumulative_heat_flux_load') + else: + plume_status.append('pass') + plume_failure_mode.append('none') + + report_results['PlumeStatus'] = plume_status + report_results['PlumeFailureMode'] = plume_failure_mode + self.graph_mission_report(report_results) + + def run_axial_overshoot_sweep(self, sweep_vars, lm, tv): + """ + Simple variable sweep study that assesses RCS performance for a given set of axial overshoot velocity values. + """ + + # Organize variables to sweet over. + axial_overshoot = sweep_vars['axial_overshoot'] + self.max_v0 = np.max(axial_overshoot) + + # Link elements for RPOD analysis. + self.init_trade_study(lm, tv) + + # Create results directory if necessary. + results_dir = self.case_dir + 'results' + if not os.path.isdir(results_dir): + os.mkdir(results_dir) + + # Loop through over shoot velocities to test. + for i, v_o in enumerate(axial_overshoot): + + # print(i, v_o) + # # Set unique case identifier within trade study. + self.rpod.set_case_key(i, 0) + + # Create JFH for a given velocity. + self.rpod.print_jfh_1d_approach_n_fire( + tv.v_ida, + v_o, + tv.r_o, + n_firings = 100, + trade_study = True + ) + + # Reset JFH according to specific case. + self.init_trade_study_case() + self.rpod.graph_jfh(trade_study= True) + self.rpod.jfh_plume_strikes(trade_study = True) + + self.print_mission_report() + self.interpret_mission_report() + + + # self.rpod.jfh_plume_strikes(trade_study = True) + + def run_surface_cant_sweep(self, sweep_vars, lm, tv): + """ + """ + + # Organize variables to sweet over. + surface_cant_angles = sweep_vars['surface_cant_angles'] + v_o = sweep_vars['axial_overshoot'] + self.max_v0 = np.max(v_o) + + # Link elements for RPOD analysis. + self.init_trade_study(lm, tv) + + angle_sweep = SweepConfig.SweepDecelAngles(lm.thruster_data, lm.rcs_groups) + + # Create results directory if necessary. + results_dir = self.case_dir + 'results' + if not os.path.isdir(results_dir): + os.mkdir(results_dir) + + # Loop through over shoot velocities to test. + for i, cant in enumerate(surface_cant_angles): + + cant = np.deg2rad(cant) + lm.decel_cant = cant + + new_tcd = angle_sweep.cant_decel_thrusters(cant) + lm.set_thruster_config(new_tcd) + + # print(i, v_o) + # # Set unique case identifier within trade study. + self.rpod.set_case_key(0, i) + + # Create JFH for a given velocity. + self.rpod.print_jfh_1d_approach_n_fire( + tv.v_ida, + v_o, + tv.r_o, + n_firings = 100, + trade_study = True + ) + + # Reset JFH according to specific case. + self.init_trade_study_case() + + self.rpod.graph_jfh(trade_study= True) + self.rpod.jfh_plume_strikes(trade_study = True) + + self.print_mission_report() + self.interpret_mission_report() + + def run_multi_var_sweep(self, sweep_vars, lm, tv): + """ + """ + # Organize variables to sweet over. + axial_overshoot = sweep_vars['axial_overshoot'] + surface_cant_angles = sweep_vars['surface_cant_angles'] + self.max_v0 = np.max(axial_overshoot) + + # Link elements for RPOD analysis. + self.init_trade_study(lm, tv) + + angle_sweep = SweepConfig.SweepDecelAngles(lm.thruster_data, lm.rcs_groups) + + # Create results directory if necessary. + results_dir = self.case_dir + 'results' + if not os.path.isdir(results_dir): + os.mkdir(results_dir) + + # Loop through over shoot velocities to test. + for i, v_o in enumerate(axial_overshoot): + for j, cant in enumerate(surface_cant_angles): + + lm.decel_cant = cant + + new_tcd = angle_sweep.cant_decel_thrusters(cant) + lm.set_thruster_config(new_tcd) + + # print(i, v_o) + # # Set unique case identifier within trade study. + self.rpod.set_case_key(i, j) + + # Create JFH for a given velocity. + self.rpod.print_jfh_1d_approach_n_fire( + tv.v_ida, + v_o, + tv.r_o, + n_firings = 10, + trade_study = True + ) + + # Reset JFH according to specific case. + self.init_trade_study_case() + + self.rpod.graph_jfh(trade_study= True) + self.rpod.jfh_plume_strikes(trade_study = True) + + self.print_mission_report() self.interpret_mission_report() \ No newline at end of file diff --git a/pyrpod/plume/PlumeStrikeCalculator.py b/pyrpod/plume/PlumeStrikeCalculator.py index bbadf98..755f8c9 100644 --- a/pyrpod/plume/PlumeStrikeCalculator.py +++ b/pyrpod/plume/PlumeStrikeCalculator.py @@ -1,417 +1,417 @@ -""" -Plume impingement computations for RPOD. - -Responsibilities: -- Given target mesh, VV pose, and active thrusters, compute per-face strike metrics -- Return numpy arrays/dicts; do not write files - -This consolidates logic currently in RPOD.jfh_plume_strikes into -reusable, testable functions. - -Implementation notes: -- compute_plume_strikes() runs a NumPy-vectorized strike-detection path by - default. _compute_plume_strikes_scalar() preserves the original per-face - loop verbatim as a reference implementation for tests and benchmarking. -- The vectorized core operates on plain serializable inputs (arrays, dicts, - floats) so it can also run inside process-based workers. - -Future work (no new dependencies planned): -- Vectorize the SimplifiedGasKinetics evaluations for struck faces. -- Shared-memory arrays (multiprocessing.shared_memory) for very large meshes. -- Chunking strategy to batch many small firings per worker task. -""" -from __future__ import annotations - -from concurrent.futures import ProcessPoolExecutor -from typing import Any, Dict, List, Optional, Sequence - -import numpy as np -from pyrpod.plume.RarefiedPlumeGasKinetics import SimplifiedGasKinetics - - -def compute_face_centroids(vectors: np.ndarray) -> np.ndarray: - """Compute per-face centroids for an (N x 3 x 3) array of face vertices. - - Averages the three vertices of each face, matching the scalar reference - (mean over each coordinate in the face's native dtype). The target is - stationary during a run, so callers should compute this once and pass it - to compute_plume_strikes() via face_centroids. - """ - return np.asarray(vectors).mean(axis=1) - - -def _build_thruster_link(thruster_data: Dict[str, Any]) -> Dict[str, Any]: - """Map numeric JFH thruster indices ('1', '2', ...) to thruster names, - consistent with legacy ordering of the thruster configuration.""" - link = {} - i = 1 - for thruster in thruster_data: - link[str(i)] = thruster_data[thruster]['name'] - i += 1 - return link - - -def extract_plume_params(environment: Any) -> Dict[str, Any]: - """Extract the plain config values needed for strike computation. - - Returns a picklable dict (radius, wedge_theta, use_kinetics, and — only - when kinetics is enabled — surface_temp and sigma) so workers never need - the full environment object. - """ - config = environment.config - use_kinetics = config['pm']['kinetics'] != 'None' - params: Dict[str, Any] = { - 'radius': float(config['plume']['radius']), - 'wedge_theta': float(config['plume']['wedge_theta']), - 'use_kinetics': use_kinetics, - 'surface_temp': None, - 'sigma': None, - } - if use_kinetics: - params['surface_temp'] = float(config['tv']['surface_temp']) - params['sigma'] = float(config['tv']['sigma']) - return params - - -def _compute_plume_strikes_core( - face_centroids: np.ndarray, - target_unit_normals: np.ndarray, - thruster_data: Dict[str, Any], - thruster_metrics: Optional[Dict[str, Any]], - jfh_step: Dict[str, Any], - plume_params: Dict[str, Any], -) -> Dict[str, np.ndarray]: - """Vectorized strike computation on plain serializable inputs. - - Geometry is evaluated with NumPy over all faces per active thruster. - Gas-kinetics quantities remain scalar: SimplifiedGasKinetics is - instantiated only for struck face indices, exactly as in the scalar - reference. Memory scales with the number of faces (a few (N,) and (N,3) - temporaries), independent of the number of firings. - """ - num_faces = len(face_centroids) - strikes = np.zeros(num_faces) - - use_kinetics = plume_params['use_kinetics'] - if use_kinetics: - pressures = np.zeros(num_faces) - shear_stresses = np.zeros(num_faces) - heat_flux = np.zeros(num_faces) - heat_flux_load = np.zeros(num_faces) - - vv_pos = np.array(jfh_step['xyz']) - vv_orientation = np.array(jfh_step['dcm']).transpose() - thrusters = jfh_step['thrusters'] - firing_time = float(jfh_step['t']) if 't' in jfh_step else 0.0 - - link = _build_thruster_link(thruster_data) - - plume_radius = float(plume_params['radius']) - wedge_theta = float(plume_params['wedge_theta']) - - normals = np.asarray(target_unit_normals) - - for thr in thrusters: - thruster_id = link[str(thr)][0] - - thruster_orientation = np.array(thruster_data[thruster_id]['dcm']).transpose() - thruster_orientation = thruster_orientation.dot(vv_orientation) - plume_normal = np.array(thruster_orientation[0]) - norm_plume_normal = np.linalg.norm(plume_normal) - unit_plume_normal = plume_normal / norm_plume_normal - - thr_exit = np.array(thruster_data[thruster_id]['exit']) - thruster_pos = vv_pos + thr_exit - thruster_pos = thruster_pos[0] - - distance = thruster_pos - face_centroids - norm_distance = np.linalg.norm(distance, axis=1) - - # Faces whose centroid coincides with the thruster exit are skipped, - # matching the scalar reference's `norm_distance == 0` guard. - valid = norm_distance != 0.0 - unit_distance = np.zeros_like(distance) - np.divide( - distance, - norm_distance[:, np.newaxis], - out=unit_distance, - where=valid[:, np.newaxis], - ) - - # NOTE: 3.14 (not np.pi) is kept deliberately to reproduce the legacy - # scalar reference bit-for-bit; changing it shifts theta by ~1.6e-3 rad - # and can alter struck-face IDs near the wedge boundary. - theta = 3.14 - np.arccos((unit_distance * unit_plume_normal).sum(axis=1)) - - surface_dot_plume = (normals * unit_plume_normal).sum(axis=1) - - hit = ( - valid - & (norm_distance < plume_radius) - & (theta < wedge_theta) - & (surface_dot_plume < 0) - ) - - strikes[hit] += 1 - - if use_kinetics: - T_w = plume_params['surface_temp'] - sigma = plume_params['sigma'] - t_type = thruster_data[thruster_id]['type'][0] - metrics = thruster_metrics[t_type] - for idx in np.nonzero(hit)[0]: - simple_plume = SimplifiedGasKinetics( - norm_distance[idx], theta[idx], metrics, T_w, sigma - ) - pressures[idx] += simple_plume.get_pressure() - shear = simple_plume.get_shear_pressure() - shear_stresses[idx] += abs(shear) - hf = simple_plume.get_heat_flux() - heat_flux[idx] += hf - heat_flux_load[idx] += hf * firing_time - - result = {"strikes": strikes} - if use_kinetics: - result.update({ - "pressures": pressures, - "shear_stress": shear_stresses, - "heat_flux_rate": heat_flux, - "heat_flux_load": heat_flux_load, - }) - return result - - -def compute_plume_strikes( - target_mesh: Any, - target_unit_normals: np.ndarray, - vv: Any, - jfh_step: Dict[str, Any], - environment: Any, - face_centroids: Optional[np.ndarray] = None, -) -> Dict[str, np.ndarray]: - """Compute plume strike arrays for a single JFH step. - - Inputs - - target_mesh: numpy-stl Mesh-like, exposes .vectors (N x 3 x 3) - - target_unit_normals: (N x 3) array of per-face unit normals - - vv: Visiting vehicle with thruster_data and thruster_metrics - - jfh_step: dict with keys 'thrusters' (list[int]), 'xyz' (pos), 'dcm' (3x3) - - environment: provides config for plume and kinetics - - face_centroids: optional (N x 3) precomputed face centroids - (see compute_face_centroids). When the target is stationary, callers - should compute centroids once per run and pass them here; if omitted, - they are computed from target_mesh for this step. - - Returns - - dict with per-face arrays for current step: strikes and optionally pressures, shear_stress, heat_flux_rate, heat_flux_load - """ - if face_centroids is None: - face_centroids = compute_face_centroids(target_mesh.vectors) - plume_params = extract_plume_params(environment) - return _compute_plume_strikes_core( - face_centroids=face_centroids, - target_unit_normals=target_unit_normals, - thruster_data=vv.thruster_data, - # Only defined/needed when kinetics is enabled; the core only reads it - # for struck faces, matching the scalar reference. - thruster_metrics=getattr(vv, 'thruster_metrics', None), - jfh_step=jfh_step, - plume_params=plume_params, - ) - - -def _compute_plume_strikes_scalar( - target_mesh: Any, - target_unit_normals: np.ndarray, - vv: Any, - jfh_step: Dict[str, Any], - environment: Any, -) -> Dict[str, np.ndarray]: - """Scalar reference implementation of compute_plume_strikes(). - - Preserved verbatim from the original per-face loop. Kept for regression - tests, debugging, and benchmarking against the vectorized path; the two - must produce identical strike arrays and struck-face IDs. - """ - num_faces = len(target_mesh.vectors) - strikes = np.zeros(num_faces) - - use_kinetics = environment.config['pm']['kinetics'] != 'None' - if use_kinetics: - pressures = np.zeros(num_faces) - shear_stresses = np.zeros(num_faces) - heat_flux = np.zeros(num_faces) - heat_flux_load = np.zeros(num_faces) - - vv_pos = np.array(jfh_step['xyz']) - vv_orientation = np.array(jfh_step['dcm']).transpose() - thrusters = jfh_step['thrusters'] - firing_time = float(jfh_step['t']) if 't' in jfh_step else 0.0 - - # Build mapping from numeric JFH indices to thruster ids consistent with legacy - link = {} - i = 1 - for thruster in vv.thruster_data: - link[str(i)] = vv.thruster_data[thruster]['name'] - i += 1 - - plume_radius = float(environment.config['plume']['radius']) - wedge_theta = float(environment.config['plume']['wedge_theta']) - - for thr in thrusters: - thruster_id = link[str(thr)][0] - - thruster_orientation = np.array(vv.thruster_data[thruster_id]['dcm']).transpose() - thruster_orientation = thruster_orientation.dot(vv_orientation) - plume_normal = np.array(thruster_orientation[0]) - norm_plume_normal = np.linalg.norm(plume_normal) - unit_plume_normal = plume_normal / norm_plume_normal - - thr_exit = np.array(vv.thruster_data[thruster_id]['exit']) - thruster_pos = vv_pos + thr_exit - thruster_pos = thruster_pos[0] - - for idx, face in enumerate(target_mesh.vectors): - face = np.array(face).transpose() - centroid = np.array([face[0].mean(), face[1].mean(), face[2].mean()]) - distance = thruster_pos - centroid - norm_distance = np.linalg.norm(distance) - if norm_distance == 0: - continue - unit_distance = distance / norm_distance - - theta = 3.14 - np.arccos(np.dot(np.squeeze(unit_distance), np.squeeze(unit_plume_normal))) - - n = np.squeeze(target_unit_normals[idx]) - unit_plume = np.squeeze(plume_normal / norm_plume_normal) - surface_dot_plume = np.dot(n, unit_plume) - - within_distance = float(norm_distance) < plume_radius - within_theta = float(theta) < wedge_theta - facing_thruster = surface_dot_plume < 0 - - if within_distance and within_theta and facing_thruster: - strikes[idx] += 1 - if use_kinetics: - T_w = float(environment.config['tv']['surface_temp']) - sigma = float(environment.config['tv']['sigma']) - t_type = vv.thruster_data[thruster_id]['type'][0] - thruster_metrics = vv.thruster_metrics[t_type] - simple_plume = SimplifiedGasKinetics(norm_distance, theta, thruster_metrics, T_w, sigma) - pressures[idx] += simple_plume.get_pressure() - shear = simple_plume.get_shear_pressure() - shear_stresses[idx] += abs(shear) - hf = simple_plume.get_heat_flux() - heat_flux[idx] += hf - heat_flux_load[idx] += hf * firing_time - - result = {"strikes": strikes} - if use_kinetics: - result.update({ - "pressures": pressures, - "shear_stress": shear_stresses, - "heat_flux_rate": heat_flux, - "heat_flux_load": heat_flux_load, - }) - return result - - -# Per-process state for parallel workers. Populated once per worker by -# _parallel_worker_init so the (N,3) target arrays are shipped to each worker -# a single time instead of once per submitted firing. Memory therefore scales -# with workers x faces, never firings x faces. -_WORKER_STATE: Dict[str, Any] = {} - - -def _parallel_worker_init( - face_centroids: np.ndarray, - target_unit_normals: np.ndarray, - thruster_data: Dict[str, Any], - thruster_metrics: Optional[Dict[str, Any]], - plume_params: Dict[str, Any], -) -> None: - """ProcessPoolExecutor initializer: cache shared per-run inputs.""" - _WORKER_STATE['face_centroids'] = face_centroids - _WORKER_STATE['target_unit_normals'] = target_unit_normals - _WORKER_STATE['thruster_data'] = thruster_data - _WORKER_STATE['thruster_metrics'] = thruster_metrics - _WORKER_STATE['plume_params'] = plume_params - - -def _parallel_worker_compute(task) -> Any: - """Compute strikes for one firing inside a worker process. - - task is (firing_index, jfh_step); returns (firing_index, result dict). - """ - firing_index, jfh_step = task - result = _compute_plume_strikes_core( - face_centroids=_WORKER_STATE['face_centroids'], - target_unit_normals=_WORKER_STATE['target_unit_normals'], - thruster_data=_WORKER_STATE['thruster_data'], - thruster_metrics=_WORKER_STATE['thruster_metrics'], - jfh_step=jfh_step, - plume_params=_WORKER_STATE['plume_params'], - ) - return firing_index, result - - -def run_parallel_plume_strikes( - jfh_steps: Sequence[Dict[str, Any]], - face_centroids: np.ndarray, - target_unit_normals: np.ndarray, - thruster_data: Dict[str, Any], - thruster_metrics: Optional[Dict[str, Any]], - plume_params: Dict[str, Any], - workers: int, -) -> List[Dict[str, np.ndarray]]: - """Compute per-firing strike results across processes, one firing per task. - - All inputs must be plain serializable data (NumPy arrays, dicts, - primitives) — full study/vehicle/environment objects are never pickled. - Results are returned as a list indexed by firing, preserving JFH order - regardless of completion order; cumulative accumulation and VTK output - remain the caller's responsibility (serial, in the parent process). - - Raises whatever the executor or workers raise; callers are expected to - fall back to the serial path with a clear message. - """ - results: List[Optional[Dict[str, np.ndarray]]] = [None] * len(jfh_steps) - with ProcessPoolExecutor( - max_workers=workers, - initializer=_parallel_worker_init, - initargs=( - face_centroids, - target_unit_normals, - thruster_data, - thruster_metrics, - plume_params, - ), - ) as executor: - futures = [ - executor.submit(_parallel_worker_compute, (i, step)) - for i, step in enumerate(jfh_steps) - ] - for future in futures: - firing_index, result = future.result() - results[firing_index] = result - return results - - -def accumulate_cumulative( - cumulative: Dict[str, np.ndarray], - current: Dict[str, np.ndarray], -) -> Dict[str, np.ndarray]: - """Accumulate per-step arrays into cumulative tallies (e.g., cum_strikes, max_pressures).""" - if "cum_strikes" in cumulative and "strikes" in current: - cumulative["cum_strikes"] = cumulative["cum_strikes"] + current["strikes"] - - # Max trackers if available - if "max_pressures" in cumulative and "pressures" in current: - cumulative["max_pressures"] = np.maximum(cumulative["max_pressures"], current["pressures"]) - if "max_shears" in cumulative and "shear_stress" in current: - cumulative["max_shears"] = np.maximum(cumulative["max_shears"], current["shear_stress"]) - - if "cum_heat_flux_load" in cumulative and "heat_flux_load" in current: - cumulative["cum_heat_flux_load"] = cumulative["cum_heat_flux_load"] + current["heat_flux_load"] - - return cumulative +""" +Plume impingement computations for RPOD. + +Responsibilities: +- Given target mesh, VV pose, and active thrusters, compute per-face strike metrics +- Return numpy arrays/dicts; do not write files + +This consolidates logic currently in RPOD.jfh_plume_strikes into +reusable, testable functions. + +Implementation notes: +- compute_plume_strikes() runs a NumPy-vectorized strike-detection path by + default. _compute_plume_strikes_scalar() preserves the original per-face + loop verbatim as a reference implementation for tests and benchmarking. +- The vectorized core operates on plain serializable inputs (arrays, dicts, + floats) so it can also run inside process-based workers. + +Future work (no new dependencies planned): +- Vectorize the SimplifiedGasKinetics evaluations for struck faces. +- Shared-memory arrays (multiprocessing.shared_memory) for very large meshes. +- Chunking strategy to batch many small firings per worker task. +""" +from __future__ import annotations + +from concurrent.futures import ProcessPoolExecutor +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +from pyrpod.plume.RarefiedPlumeGasKinetics import SimplifiedGasKinetics + + +def compute_face_centroids(vectors: np.ndarray) -> np.ndarray: + """Compute per-face centroids for an (N x 3 x 3) array of face vertices. + + Averages the three vertices of each face, matching the scalar reference + (mean over each coordinate in the face's native dtype). The target is + stationary during a run, so callers should compute this once and pass it + to compute_plume_strikes() via face_centroids. + """ + return np.asarray(vectors).mean(axis=1) + + +def _build_thruster_link(thruster_data: Dict[str, Any]) -> Dict[str, Any]: + """Map numeric JFH thruster indices ('1', '2', ...) to thruster names, + consistent with legacy ordering of the thruster configuration.""" + link = {} + i = 1 + for thruster in thruster_data: + link[str(i)] = thruster_data[thruster]['name'] + i += 1 + return link + + +def extract_plume_params(environment: Any) -> Dict[str, Any]: + """Extract the plain config values needed for strike computation. + + Returns a picklable dict (radius, wedge_theta, use_kinetics, and — only + when kinetics is enabled — surface_temp and sigma) so workers never need + the full environment object. + """ + config = environment.config + use_kinetics = config['pm']['kinetics'] != 'None' + params: Dict[str, Any] = { + 'radius': float(config['plume']['radius']), + 'wedge_theta': float(config['plume']['wedge_theta']), + 'use_kinetics': use_kinetics, + 'surface_temp': None, + 'sigma': None, + } + if use_kinetics: + params['surface_temp'] = float(config['tv']['surface_temp']) + params['sigma'] = float(config['tv']['sigma']) + return params + + +def _compute_plume_strikes_core( + face_centroids: np.ndarray, + target_unit_normals: np.ndarray, + thruster_data: Dict[str, Any], + thruster_metrics: Optional[Dict[str, Any]], + jfh_step: Dict[str, Any], + plume_params: Dict[str, Any], +) -> Dict[str, np.ndarray]: + """Vectorized strike computation on plain serializable inputs. + + Geometry is evaluated with NumPy over all faces per active thruster. + Gas-kinetics quantities remain scalar: SimplifiedGasKinetics is + instantiated only for struck face indices, exactly as in the scalar + reference. Memory scales with the number of faces (a few (N,) and (N,3) + temporaries), independent of the number of firings. + """ + num_faces = len(face_centroids) + strikes = np.zeros(num_faces) + + use_kinetics = plume_params['use_kinetics'] + if use_kinetics: + pressures = np.zeros(num_faces) + shear_stresses = np.zeros(num_faces) + heat_flux = np.zeros(num_faces) + heat_flux_load = np.zeros(num_faces) + + vv_pos = np.array(jfh_step['xyz']) + vv_orientation = np.array(jfh_step['dcm']).transpose() + thrusters = jfh_step['thrusters'] + firing_time = float(jfh_step['t']) if 't' in jfh_step else 0.0 + + link = _build_thruster_link(thruster_data) + + plume_radius = float(plume_params['radius']) + wedge_theta = float(plume_params['wedge_theta']) + + normals = np.asarray(target_unit_normals) + + for thr in thrusters: + thruster_id = link[str(thr)][0] + + thruster_orientation = np.array(thruster_data[thruster_id]['dcm']).transpose() + thruster_orientation = thruster_orientation.dot(vv_orientation) + plume_normal = np.array(thruster_orientation[0]) + norm_plume_normal = np.linalg.norm(plume_normal) + unit_plume_normal = plume_normal / norm_plume_normal + + thr_exit = np.array(thruster_data[thruster_id]['exit']) + thruster_pos = vv_pos + thr_exit + thruster_pos = thruster_pos[0] + + distance = thruster_pos - face_centroids + norm_distance = np.linalg.norm(distance, axis=1) + + # Faces whose centroid coincides with the thruster exit are skipped, + # matching the scalar reference's `norm_distance == 0` guard. + valid = norm_distance != 0.0 + unit_distance = np.zeros_like(distance) + np.divide( + distance, + norm_distance[:, np.newaxis], + out=unit_distance, + where=valid[:, np.newaxis], + ) + + # NOTE: 3.14 (not np.pi) is kept deliberately to reproduce the legacy + # scalar reference bit-for-bit; changing it shifts theta by ~1.6e-3 rad + # and can alter struck-face IDs near the wedge boundary. + theta = 3.14 - np.arccos((unit_distance * unit_plume_normal).sum(axis=1)) + + surface_dot_plume = (normals * unit_plume_normal).sum(axis=1) + + hit = ( + valid + & (norm_distance < plume_radius) + & (theta < wedge_theta) + & (surface_dot_plume < 0) + ) + + strikes[hit] += 1 + + if use_kinetics: + T_w = plume_params['surface_temp'] + sigma = plume_params['sigma'] + t_type = thruster_data[thruster_id]['type'][0] + metrics = thruster_metrics[t_type] + for idx in np.nonzero(hit)[0]: + simple_plume = SimplifiedGasKinetics( + norm_distance[idx], theta[idx], metrics, T_w, sigma + ) + pressures[idx] += simple_plume.get_pressure() + shear = simple_plume.get_shear_pressure() + shear_stresses[idx] += abs(shear) + hf = simple_plume.get_heat_flux() + heat_flux[idx] += hf + heat_flux_load[idx] += hf * firing_time + + result = {"strikes": strikes} + if use_kinetics: + result.update({ + "pressures": pressures, + "shear_stress": shear_stresses, + "heat_flux_rate": heat_flux, + "heat_flux_load": heat_flux_load, + }) + return result + + +def compute_plume_strikes( + target_mesh: Any, + target_unit_normals: np.ndarray, + vv: Any, + jfh_step: Dict[str, Any], + environment: Any, + face_centroids: Optional[np.ndarray] = None, +) -> Dict[str, np.ndarray]: + """Compute plume strike arrays for a single JFH step. + + Inputs + - target_mesh: numpy-stl Mesh-like, exposes .vectors (N x 3 x 3) + - target_unit_normals: (N x 3) array of per-face unit normals + - vv: Visiting vehicle with thruster_data and thruster_metrics + - jfh_step: dict with keys 'thrusters' (list[int]), 'xyz' (pos), 'dcm' (3x3) + - environment: provides config for plume and kinetics + - face_centroids: optional (N x 3) precomputed face centroids + (see compute_face_centroids). When the target is stationary, callers + should compute centroids once per run and pass them here; if omitted, + they are computed from target_mesh for this step. + + Returns + - dict with per-face arrays for current step: strikes and optionally pressures, shear_stress, heat_flux_rate, heat_flux_load + """ + if face_centroids is None: + face_centroids = compute_face_centroids(target_mesh.vectors) + plume_params = extract_plume_params(environment) + return _compute_plume_strikes_core( + face_centroids=face_centroids, + target_unit_normals=target_unit_normals, + thruster_data=vv.thruster_data, + # Only defined/needed when kinetics is enabled; the core only reads it + # for struck faces, matching the scalar reference. + thruster_metrics=getattr(vv, 'thruster_metrics', None), + jfh_step=jfh_step, + plume_params=plume_params, + ) + + +def _compute_plume_strikes_scalar( + target_mesh: Any, + target_unit_normals: np.ndarray, + vv: Any, + jfh_step: Dict[str, Any], + environment: Any, +) -> Dict[str, np.ndarray]: + """Scalar reference implementation of compute_plume_strikes(). + + Preserved verbatim from the original per-face loop. Kept for regression + tests, debugging, and benchmarking against the vectorized path; the two + must produce identical strike arrays and struck-face IDs. + """ + num_faces = len(target_mesh.vectors) + strikes = np.zeros(num_faces) + + use_kinetics = environment.config['pm']['kinetics'] != 'None' + if use_kinetics: + pressures = np.zeros(num_faces) + shear_stresses = np.zeros(num_faces) + heat_flux = np.zeros(num_faces) + heat_flux_load = np.zeros(num_faces) + + vv_pos = np.array(jfh_step['xyz']) + vv_orientation = np.array(jfh_step['dcm']).transpose() + thrusters = jfh_step['thrusters'] + firing_time = float(jfh_step['t']) if 't' in jfh_step else 0.0 + + # Build mapping from numeric JFH indices to thruster ids consistent with legacy + link = {} + i = 1 + for thruster in vv.thruster_data: + link[str(i)] = vv.thruster_data[thruster]['name'] + i += 1 + + plume_radius = float(environment.config['plume']['radius']) + wedge_theta = float(environment.config['plume']['wedge_theta']) + + for thr in thrusters: + thruster_id = link[str(thr)][0] + + thruster_orientation = np.array(vv.thruster_data[thruster_id]['dcm']).transpose() + thruster_orientation = thruster_orientation.dot(vv_orientation) + plume_normal = np.array(thruster_orientation[0]) + norm_plume_normal = np.linalg.norm(plume_normal) + unit_plume_normal = plume_normal / norm_plume_normal + + thr_exit = np.array(vv.thruster_data[thruster_id]['exit']) + thruster_pos = vv_pos + thr_exit + thruster_pos = thruster_pos[0] + + for idx, face in enumerate(target_mesh.vectors): + face = np.array(face).transpose() + centroid = np.array([face[0].mean(), face[1].mean(), face[2].mean()]) + distance = thruster_pos - centroid + norm_distance = np.linalg.norm(distance) + if norm_distance == 0: + continue + unit_distance = distance / norm_distance + + theta = 3.14 - np.arccos(np.dot(np.squeeze(unit_distance), np.squeeze(unit_plume_normal))) + + n = np.squeeze(target_unit_normals[idx]) + unit_plume = np.squeeze(plume_normal / norm_plume_normal) + surface_dot_plume = np.dot(n, unit_plume) + + within_distance = float(norm_distance) < plume_radius + within_theta = float(theta) < wedge_theta + facing_thruster = surface_dot_plume < 0 + + if within_distance and within_theta and facing_thruster: + strikes[idx] += 1 + if use_kinetics: + T_w = float(environment.config['tv']['surface_temp']) + sigma = float(environment.config['tv']['sigma']) + t_type = vv.thruster_data[thruster_id]['type'][0] + thruster_metrics = vv.thruster_metrics[t_type] + simple_plume = SimplifiedGasKinetics(norm_distance, theta, thruster_metrics, T_w, sigma) + pressures[idx] += simple_plume.get_pressure() + shear = simple_plume.get_shear_pressure() + shear_stresses[idx] += abs(shear) + hf = simple_plume.get_heat_flux() + heat_flux[idx] += hf + heat_flux_load[idx] += hf * firing_time + + result = {"strikes": strikes} + if use_kinetics: + result.update({ + "pressures": pressures, + "shear_stress": shear_stresses, + "heat_flux_rate": heat_flux, + "heat_flux_load": heat_flux_load, + }) + return result + + +# Per-process state for parallel workers. Populated once per worker by +# _parallel_worker_init so the (N,3) target arrays are shipped to each worker +# a single time instead of once per submitted firing. Memory therefore scales +# with workers x faces, never firings x faces. +_WORKER_STATE: Dict[str, Any] = {} + + +def _parallel_worker_init( + face_centroids: np.ndarray, + target_unit_normals: np.ndarray, + thruster_data: Dict[str, Any], + thruster_metrics: Optional[Dict[str, Any]], + plume_params: Dict[str, Any], +) -> None: + """ProcessPoolExecutor initializer: cache shared per-run inputs.""" + _WORKER_STATE['face_centroids'] = face_centroids + _WORKER_STATE['target_unit_normals'] = target_unit_normals + _WORKER_STATE['thruster_data'] = thruster_data + _WORKER_STATE['thruster_metrics'] = thruster_metrics + _WORKER_STATE['plume_params'] = plume_params + + +def _parallel_worker_compute(task) -> Any: + """Compute strikes for one firing inside a worker process. + + task is (firing_index, jfh_step); returns (firing_index, result dict). + """ + firing_index, jfh_step = task + result = _compute_plume_strikes_core( + face_centroids=_WORKER_STATE['face_centroids'], + target_unit_normals=_WORKER_STATE['target_unit_normals'], + thruster_data=_WORKER_STATE['thruster_data'], + thruster_metrics=_WORKER_STATE['thruster_metrics'], + jfh_step=jfh_step, + plume_params=_WORKER_STATE['plume_params'], + ) + return firing_index, result + + +def run_parallel_plume_strikes( + jfh_steps: Sequence[Dict[str, Any]], + face_centroids: np.ndarray, + target_unit_normals: np.ndarray, + thruster_data: Dict[str, Any], + thruster_metrics: Optional[Dict[str, Any]], + plume_params: Dict[str, Any], + workers: int, +) -> List[Dict[str, np.ndarray]]: + """Compute per-firing strike results across processes, one firing per task. + + All inputs must be plain serializable data (NumPy arrays, dicts, + primitives) — full study/vehicle/environment objects are never pickled. + Results are returned as a list indexed by firing, preserving JFH order + regardless of completion order; cumulative accumulation and VTK output + remain the caller's responsibility (serial, in the parent process). + + Raises whatever the executor or workers raise; callers are expected to + fall back to the serial path with a clear message. + """ + results: List[Optional[Dict[str, np.ndarray]]] = [None] * len(jfh_steps) + with ProcessPoolExecutor( + max_workers=workers, + initializer=_parallel_worker_init, + initargs=( + face_centroids, + target_unit_normals, + thruster_data, + thruster_metrics, + plume_params, + ), + ) as executor: + futures = [ + executor.submit(_parallel_worker_compute, (i, step)) + for i, step in enumerate(jfh_steps) + ] + for future in futures: + firing_index, result = future.result() + results[firing_index] = result + return results + + +def accumulate_cumulative( + cumulative: Dict[str, np.ndarray], + current: Dict[str, np.ndarray], +) -> Dict[str, np.ndarray]: + """Accumulate per-step arrays into cumulative tallies (e.g., cum_strikes, max_pressures).""" + if "cum_strikes" in cumulative and "strikes" in current: + cumulative["cum_strikes"] = cumulative["cum_strikes"] + current["strikes"] + + # Max trackers if available + if "max_pressures" in cumulative and "pressures" in current: + cumulative["max_pressures"] = np.maximum(cumulative["max_pressures"], current["pressures"]) + if "max_shears" in cumulative and "shear_stress" in current: + cumulative["max_shears"] = np.maximum(cumulative["max_shears"], current["shear_stress"]) + + if "cum_heat_flux_load" in cumulative and "heat_flux_load" in current: + cumulative["cum_heat_flux_load"] = cumulative["cum_heat_flux_load"] + current["heat_flux_load"] + + return cumulative diff --git a/pyrpod/rpod/JetFiringHistory.py b/pyrpod/rpod/JetFiringHistory.py index 799a5fc..6e210d2 100644 --- a/pyrpod/rpod/JetFiringHistory.py +++ b/pyrpod/rpod/JetFiringHistory.py @@ -1,377 +1,377 @@ -import configparser -import numpy as np -import sympy as sp - -from pyrpod.util.io.file_print import print_JFH -from pyrpod.logging_utils import get_logger -from pyrpod.util.math.transform import rotation_matrix_from_vectors -from pyrpod.util.io.fs import resolve_asset_path - -logger = get_logger("pyrpod.rpod.JetFiringHistory") - -def make_norm(vector_value_function): - """Calculate vector norm/magnitude using the Pythagoream Theorem.""" - return sp.sqrt(sp.Pow(vector_value_function[0],2) + sp.Pow(vector_value_function[1],2)) - -class JetFiringHistory: - """ - Class responsible for reading and parsing through text files - that contain jet firing histories for a visiting vehicle. - - Attributes - ---------- - config : ConfigParser - Object responsible for reading data from the provided configuration file. - - case_dir : str - Path to case directory. Used to store data and results for a specific scenario. - - JFH : list - List containing information for each firing (stored as a dict) in the JFH. - - Methods - ------- - read_JFH() - Method reads in and parses through text file containing the JFH. - - graph_param_curve(self, t, r_of_t) - Used to quickly prototype and visualize a proposed approach path. - Calculates the unit tangent vector at a given timestep and - rotates the STL file accordingly. Data is plotted using matlab - - print_JFH_param_curve(self, jfh_path, t, r_of_t, align = False) - Used to produce JFH data for a proposed approach path. - Calculates the unit tangent vector at a given timestep and DCMs for - STL file rotations. Data is then saved to a text file. - """ - - def __init__(self, case_dir): - """ - Constructor simply sets case directory and parses the - appopriate configuration file. - - Parameters - ---------- - case_dir : str - Path to case directory. Used to store data and results for a specific scenario. - - Returns - ------- - jfh : JetFiringHistory - Method reads in and parses through text file containing the JFH. - """ - - self.case_dir = case_dir - config = configparser.ConfigParser() - config.read(self.case_dir + "config.ini") - self.config = config - - def read_jfh(self): - """ - Method responsible for reading and parsing through JFH data. - - NOTE: Methods does not take any parameters. It assumes that self.case_dir - and self.config are instantiated correctly. Potential defensive programming statements? - - Parameters - ---------- - None - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - - """ - - try: - path_to_jfh = resolve_asset_path(self.case_dir, 'jfh', self.config['jfh']['jfh']) - except KeyError: - # print("WARNING: Jet Firing History not set") - self.JFH = None - return - - with open(path_to_jfh, 'r') as f: - lines = f.readlines() - - # Save number of firings in JFH. - try: - self.nt = int(lines.pop(0).split(' ')[4]) - except IndexError: - logger.warning("Supplied JFH file is empty: %s", path_to_jfh) - self.JFH = None - return - - # Throw away second line - lines.pop(0) - - JFH = [] - - for i in range(self.nt): - - # print(i) - # Split current into a list row at every space character. - curr_row = lines.pop(0).split(' ') - # print(curr_row) - - - # Remove empty strings from list. - while("" in curr_row): - curr_row.remove("") - - # Remove new line character. - curr_row[-1] = curr_row[-1].split('\n')[0] - # print(curr_row) - # Save all information in current row to a dictionary. - time_step = {} - - # Save time data. - time_step['nt'] = curr_row.pop(0) - time_step['dt'] = curr_row.pop(0) - time_step['t'] = curr_row.pop(0) - - # Throw away column 4 - curr_row.pop(0) - - # Save direction cosine matrix of thruster relative to the vehicle - dcm = [] - for i in range(3): - row = [] - for j in range(3): - row.append(float(curr_row.pop(0))) - dcm.append(row) - time_step['dcm'] = dcm - - # Save position data - pos = [] - for i in range(3): - pos.append(float(curr_row.pop(0))) - time_step['xyz'] = pos - - # Save uncertainty factor - time_step['uf'] = float(curr_row.pop(0)) - - # Save thruster data - num_thrusters = int(float(curr_row.pop(0))) - thrusters = [] - for i in range(num_thrusters): - thrusters.append(int(curr_row.pop(0))) - - time_step['thrusters'] = thrusters - - JFH.append(time_step) - self.JFH = JFH - f.close() - return - - - def graph_param_curve(self, t, r_of_t): - ''' Used to quickly prototype and visualize a proposed approach path. - Calculates the unit tangent vector at a given timestep and - rotates the STL file accordingly. Data is plotted using matlab - - Current method is old and needs updating. - - Parameters - ---------- - t : sp.symbol - Time (t) is the independent variable used to evaulte the position vector equation. - - r_of_t : list - List containing position vector expression for trajectory. - X/Y/Z positions are de-coupled and only dependent on time. - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - ''' - - t_values = np.linspace(0,2*np.pi,50) - - # Symbolic Calculations of tangent and normal unit vectors - r = r_of_t - rprime = [sp.diff(r[0], t), sp.diff(r[1], t), sp.diff(r[2], t)] - tanvector = [rprime[0] / make_norm(rprime), rprime[1] / make_norm(rprime), rprime[2] / make_norm(rprime)] - tanprime = [sp.diff(tanvector[0], t), sp.diff(tanvector[1], t), sp.diff(tanvector[2], t)] - normalvector = [tanprime[0] / make_norm(tanprime), tanprime[1] / make_norm(tanprime), tanprime[1] / make_norm(tanprime)] - tan_vector_functions = [sp.lambdify(t, tanvector[0]), sp.lambdify(t, tanvector[1]), sp.lambdify(t, tanvector[2])] - normal_vector_functions = [sp.lambdify(t, normalvector[0]), sp.lambdify(t, normalvector[1]), sp.lambdify(t, normalvector[2])] - value_functions = [sp.lambdify(t, r[0]), sp.lambdify(t, r[1]), sp.lambdify(t, r[2])] - - # Save data of evaluated position and velocity functions. - x, y, z = [value_functions[0](t_values), value_functions[1](t_values), value_functions[2](t_values)] - dx, dy, dz = [tan_vector_functions[0](t_values), tan_vector_functions[1](t_values), tan_vector_functions[2](t_values)] - - # draw the vectors along the curve and Graph STL. - for i in range(len(t_values)): - # Graph path - # ax = plt.figure().add_subplot(projection='3d') - from matplotlib import pyplot as plt - from mpl_toolkits import mplot3d - figure = plt.figure() - ax = mplot3d.Axes3D(figure) - ax.plot(x, y, z, label='position curve') - ax.legend() - normal_location = t_values[i] - - # Load, Transform, and Graph STL - from stl import mesh - VV = mesh.Mesh.from_file('../stl/cylinder.stl') - VV.points = 0.2 * VV.points - - r = [x[i], y[i], z[i]] - dr = [dx, dy, dz[i]] - - # Calculate require rotation matrix from initial orientation. - x1 = [1, 0, 0] - rot = np.array(rotation_matrix_from_vectors(x1, dr)) - - VV.rotate_using_matrix(rot.T) - VV.translate(r) - - ax.add_collection3d( - mplot3d.art3d.Poly3DCollection(VV.vectors) - ) - - # print( - # tan_vector_functions[0](normal_location), - # tan_vector_functions[1](normal_location), - # tan_vector_functions[2](normal_location) - # ) - # print() - length = 1 - ax.quiver( - value_functions[0](normal_location), - value_functions[1](normal_location), - value_functions[2](normal_location), - tan_vector_functions[0](normal_location), - tan_vector_functions[1](normal_location), - tan_vector_functions[2](normal_location), - color='g', - length = length - ) - - # ax.quiver( - # value_functions[0](normal_location), - # value_functions[1](normal_location), - # value_functions[2](normal_location), - # normal_vector_functions[0](normal_location), - # normal_vector_functions[1](normal_location), - # normal_vector_functions[2](normal_location), - # color='r' - # ) - logger.info("graph_param_curve step %s/%s", i + 1, len(t_values)) - - ax.set_xlabel('X') - ax.set_ylabel('Y') - ax.set_zlabel('Z') - - if i < 10: - index = '00' + str(i) - elif i < 100: - index = '0' + str(i) - else: - index = str(i) - - plt.savefig('img/observer-a-' + str(index) + '.png') - - plt.close() - - def print_JFH_param_curve(self, jfh_path, t, r_of_t, align = False): - ''' Used to produce JFH data for a proposed approach path. - Calculates the unit tangent vector at a given timestep and DCMs for - STL file rotations. Data is then saved to a text file. - - - Parameters - ---------- - jfh_path : str - Path to file for saving JFH data. - - t : sp.symbol - Time (t) is the independent variable used to evaulte the position vector equation. - - r_of_t : list - List containing position vector expression for trajectory. - X/Y/Z positions are de-coupled and only dependent on time. - - aligh : Boolean - Determines whether or not STL surface is rotated according to unit tangent vector. - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - ''' - - # t_values = np.linspace(0,2*np.pi,100) - t_values = np.linspace(0, 50, 20) - - # Symbolic Calculations of tangent and normal unit vectors - r = r_of_t - rprime = [sp.diff(r[0],t), sp.diff(r[1],t), sp.diff(r[2],t)] - tanvector = [rprime[0]/make_norm(rprime), rprime[1]/make_norm(rprime), rprime[2]/make_norm(rprime)] - tanprime = [sp.diff(tanvector[0],t), sp.diff(tanvector[1],t), sp.diff(tanvector[2],t)] - normalvector = [tanprime[0]/make_norm(tanprime), tanprime[1]/make_norm(tanprime), tanprime[1]/make_norm(tanprime)] - tan_vector_functions = [sp.lambdify(t, tanvector[0]),sp.lambdify(t, tanvector[1]), sp.lambdify(t, tanvector[2])] - normal_vector_functions = [sp.lambdify(t, normalvector[0]),sp.lambdify(t, normalvector[1]), sp.lambdify(t, normalvector[2])] - value_functions = [sp.lambdify(t, r[0]), sp.lambdify(t, r[1]), sp.lambdify(t, r[2])] - - # Save data of evaluated position and velocity functions. - x, y, z = [value_functions[0](t_values), value_functions[1](t_values), value_functions[2](t_values)] - dx, dy, dz = [tan_vector_functions[0](t_values), tan_vector_functions[1](t_values), tan_vector_functions[2](t_values)] - - # print(type(dx), type(dy), type(dz)) - - # print(dx.size, dy.size, dz.size) - - # When derivatives reduce to constant value the lambda function will reutrn a float - # instead of np.array. These if statements are here fill an array with that float value. - # print(type(dx), dx) - # print(type(dy), dy) - if type(x) == int: - x = np.full(t_values.size, x) - - if type(dx) == int or dx.size == 1: - # print('dx is contant') - # print(dx) - dx = np.full(t_values.size, dx) - # x = np.full(t_values.size, ) - - if type(y) == int: - y = np.full(t_values.size, y) - if type(dy) == int or dy.size == 1: - # print('dy is contant') - # print(dy) - dy = np.full(t_values.size, dy) - - if type(z) == int: - z = np.full(t_values.size, z) - if type(dz) == int or dz.size == 1: - # print('dz is contant') - # print(dz) - dz = np.full(t_values.size, dz) - - # print(type(dx), type(dy), type(dz)) - # print(type(x), type(y), type(z)) - - # Save rotation matrix for each time step - rot = [] - if align: - for i in range(len(t_values)): - dr = [dx[i], dy[i], dz[i]] - - # Calculate required rotation matrix from initial orientation. - x1 = [1, 0, 0] - rot.append(np.matrix(rotation_matrix_from_vectors(x1, dr))) - else: - for i in range(len(t_values)): - # Calculate required rotation matrix from initial orientation. - y1 = [0, 0, -1] - x1 = [1, 0, 0] - rot.append(np.matrix(rotation_matrix_from_vectors(x1, y1))) - - r = [x, y, z] - # dr = [dx, dy, dz] - print_JFH(t_values, r, rot, jfh_path) +import configparser +import numpy as np +import sympy as sp + +from pyrpod.util.io.file_print import print_JFH +from pyrpod.logging_utils import get_logger +from pyrpod.util.math.transform import rotation_matrix_from_vectors +from pyrpod.util.io.fs import resolve_asset_path + +logger = get_logger("pyrpod.rpod.JetFiringHistory") + +def make_norm(vector_value_function): + """Calculate vector norm/magnitude using the Pythagoream Theorem.""" + return sp.sqrt(sp.Pow(vector_value_function[0],2) + sp.Pow(vector_value_function[1],2)) + +class JetFiringHistory: + """ + Class responsible for reading and parsing through text files + that contain jet firing histories for a visiting vehicle. + + Attributes + ---------- + config : ConfigParser + Object responsible for reading data from the provided configuration file. + + case_dir : str + Path to case directory. Used to store data and results for a specific scenario. + + JFH : list + List containing information for each firing (stored as a dict) in the JFH. + + Methods + ------- + read_JFH() + Method reads in and parses through text file containing the JFH. + + graph_param_curve(self, t, r_of_t) + Used to quickly prototype and visualize a proposed approach path. + Calculates the unit tangent vector at a given timestep and + rotates the STL file accordingly. Data is plotted using matlab + + print_JFH_param_curve(self, jfh_path, t, r_of_t, align = False) + Used to produce JFH data for a proposed approach path. + Calculates the unit tangent vector at a given timestep and DCMs for + STL file rotations. Data is then saved to a text file. + """ + + def __init__(self, case_dir): + """ + Constructor simply sets case directory and parses the + appopriate configuration file. + + Parameters + ---------- + case_dir : str + Path to case directory. Used to store data and results for a specific scenario. + + Returns + ------- + jfh : JetFiringHistory + Method reads in and parses through text file containing the JFH. + """ + + self.case_dir = case_dir + config = configparser.ConfigParser() + config.read(self.case_dir + "config.ini") + self.config = config + + def read_jfh(self): + """ + Method responsible for reading and parsing through JFH data. + + NOTE: Methods does not take any parameters. It assumes that self.case_dir + and self.config are instantiated correctly. Potential defensive programming statements? + + Parameters + ---------- + None + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + + """ + + try: + path_to_jfh = resolve_asset_path(self.case_dir, 'jfh', self.config['jfh']['jfh']) + except KeyError: + # print("WARNING: Jet Firing History not set") + self.JFH = None + return + + with open(path_to_jfh, 'r') as f: + lines = f.readlines() + + # Save number of firings in JFH. + try: + self.nt = int(lines.pop(0).split(' ')[4]) + except IndexError: + logger.warning("Supplied JFH file is empty: %s", path_to_jfh) + self.JFH = None + return + + # Throw away second line + lines.pop(0) + + JFH = [] + + for i in range(self.nt): + + # print(i) + # Split current into a list row at every space character. + curr_row = lines.pop(0).split(' ') + # print(curr_row) + + + # Remove empty strings from list. + while("" in curr_row): + curr_row.remove("") + + # Remove new line character. + curr_row[-1] = curr_row[-1].split('\n')[0] + # print(curr_row) + # Save all information in current row to a dictionary. + time_step = {} + + # Save time data. + time_step['nt'] = curr_row.pop(0) + time_step['dt'] = curr_row.pop(0) + time_step['t'] = curr_row.pop(0) + + # Throw away column 4 + curr_row.pop(0) + + # Save direction cosine matrix of thruster relative to the vehicle + dcm = [] + for i in range(3): + row = [] + for j in range(3): + row.append(float(curr_row.pop(0))) + dcm.append(row) + time_step['dcm'] = dcm + + # Save position data + pos = [] + for i in range(3): + pos.append(float(curr_row.pop(0))) + time_step['xyz'] = pos + + # Save uncertainty factor + time_step['uf'] = float(curr_row.pop(0)) + + # Save thruster data + num_thrusters = int(float(curr_row.pop(0))) + thrusters = [] + for i in range(num_thrusters): + thrusters.append(int(curr_row.pop(0))) + + time_step['thrusters'] = thrusters + + JFH.append(time_step) + self.JFH = JFH + f.close() + return + + + def graph_param_curve(self, t, r_of_t): + ''' Used to quickly prototype and visualize a proposed approach path. + Calculates the unit tangent vector at a given timestep and + rotates the STL file accordingly. Data is plotted using matlab + + Current method is old and needs updating. + + Parameters + ---------- + t : sp.symbol + Time (t) is the independent variable used to evaulte the position vector equation. + + r_of_t : list + List containing position vector expression for trajectory. + X/Y/Z positions are de-coupled and only dependent on time. + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + ''' + + t_values = np.linspace(0,2*np.pi,50) + + # Symbolic Calculations of tangent and normal unit vectors + r = r_of_t + rprime = [sp.diff(r[0], t), sp.diff(r[1], t), sp.diff(r[2], t)] + tanvector = [rprime[0] / make_norm(rprime), rprime[1] / make_norm(rprime), rprime[2] / make_norm(rprime)] + tanprime = [sp.diff(tanvector[0], t), sp.diff(tanvector[1], t), sp.diff(tanvector[2], t)] + normalvector = [tanprime[0] / make_norm(tanprime), tanprime[1] / make_norm(tanprime), tanprime[1] / make_norm(tanprime)] + tan_vector_functions = [sp.lambdify(t, tanvector[0]), sp.lambdify(t, tanvector[1]), sp.lambdify(t, tanvector[2])] + normal_vector_functions = [sp.lambdify(t, normalvector[0]), sp.lambdify(t, normalvector[1]), sp.lambdify(t, normalvector[2])] + value_functions = [sp.lambdify(t, r[0]), sp.lambdify(t, r[1]), sp.lambdify(t, r[2])] + + # Save data of evaluated position and velocity functions. + x, y, z = [value_functions[0](t_values), value_functions[1](t_values), value_functions[2](t_values)] + dx, dy, dz = [tan_vector_functions[0](t_values), tan_vector_functions[1](t_values), tan_vector_functions[2](t_values)] + + # draw the vectors along the curve and Graph STL. + for i in range(len(t_values)): + # Graph path + # ax = plt.figure().add_subplot(projection='3d') + from matplotlib import pyplot as plt + from mpl_toolkits import mplot3d + figure = plt.figure() + ax = mplot3d.Axes3D(figure) + ax.plot(x, y, z, label='position curve') + ax.legend() + normal_location = t_values[i] + + # Load, Transform, and Graph STL + from stl import mesh + VV = mesh.Mesh.from_file('../stl/cylinder.stl') + VV.points = 0.2 * VV.points + + r = [x[i], y[i], z[i]] + dr = [dx, dy, dz[i]] + + # Calculate require rotation matrix from initial orientation. + x1 = [1, 0, 0] + rot = np.array(rotation_matrix_from_vectors(x1, dr)) + + VV.rotate_using_matrix(rot.T) + VV.translate(r) + + ax.add_collection3d( + mplot3d.art3d.Poly3DCollection(VV.vectors) + ) + + # print( + # tan_vector_functions[0](normal_location), + # tan_vector_functions[1](normal_location), + # tan_vector_functions[2](normal_location) + # ) + # print() + length = 1 + ax.quiver( + value_functions[0](normal_location), + value_functions[1](normal_location), + value_functions[2](normal_location), + tan_vector_functions[0](normal_location), + tan_vector_functions[1](normal_location), + tan_vector_functions[2](normal_location), + color='g', + length = length + ) + + # ax.quiver( + # value_functions[0](normal_location), + # value_functions[1](normal_location), + # value_functions[2](normal_location), + # normal_vector_functions[0](normal_location), + # normal_vector_functions[1](normal_location), + # normal_vector_functions[2](normal_location), + # color='r' + # ) + logger.info("graph_param_curve step %s/%s", i + 1, len(t_values)) + + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + + if i < 10: + index = '00' + str(i) + elif i < 100: + index = '0' + str(i) + else: + index = str(i) + + plt.savefig('img/observer-a-' + str(index) + '.png') + + plt.close() + + def print_JFH_param_curve(self, jfh_path, t, r_of_t, align = False): + ''' Used to produce JFH data for a proposed approach path. + Calculates the unit tangent vector at a given timestep and DCMs for + STL file rotations. Data is then saved to a text file. + + + Parameters + ---------- + jfh_path : str + Path to file for saving JFH data. + + t : sp.symbol + Time (t) is the independent variable used to evaulte the position vector equation. + + r_of_t : list + List containing position vector expression for trajectory. + X/Y/Z positions are de-coupled and only dependent on time. + + aligh : Boolean + Determines whether or not STL surface is rotated according to unit tangent vector. + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + ''' + + # t_values = np.linspace(0,2*np.pi,100) + t_values = np.linspace(0, 50, 20) + + # Symbolic Calculations of tangent and normal unit vectors + r = r_of_t + rprime = [sp.diff(r[0],t), sp.diff(r[1],t), sp.diff(r[2],t)] + tanvector = [rprime[0]/make_norm(rprime), rprime[1]/make_norm(rprime), rprime[2]/make_norm(rprime)] + tanprime = [sp.diff(tanvector[0],t), sp.diff(tanvector[1],t), sp.diff(tanvector[2],t)] + normalvector = [tanprime[0]/make_norm(tanprime), tanprime[1]/make_norm(tanprime), tanprime[1]/make_norm(tanprime)] + tan_vector_functions = [sp.lambdify(t, tanvector[0]),sp.lambdify(t, tanvector[1]), sp.lambdify(t, tanvector[2])] + normal_vector_functions = [sp.lambdify(t, normalvector[0]),sp.lambdify(t, normalvector[1]), sp.lambdify(t, normalvector[2])] + value_functions = [sp.lambdify(t, r[0]), sp.lambdify(t, r[1]), sp.lambdify(t, r[2])] + + # Save data of evaluated position and velocity functions. + x, y, z = [value_functions[0](t_values), value_functions[1](t_values), value_functions[2](t_values)] + dx, dy, dz = [tan_vector_functions[0](t_values), tan_vector_functions[1](t_values), tan_vector_functions[2](t_values)] + + # print(type(dx), type(dy), type(dz)) + + # print(dx.size, dy.size, dz.size) + + # When derivatives reduce to constant value the lambda function will reutrn a float + # instead of np.array. These if statements are here fill an array with that float value. + # print(type(dx), dx) + # print(type(dy), dy) + if type(x) == int: + x = np.full(t_values.size, x) + + if type(dx) == int or dx.size == 1: + # print('dx is contant') + # print(dx) + dx = np.full(t_values.size, dx) + # x = np.full(t_values.size, ) + + if type(y) == int: + y = np.full(t_values.size, y) + if type(dy) == int or dy.size == 1: + # print('dy is contant') + # print(dy) + dy = np.full(t_values.size, dy) + + if type(z) == int: + z = np.full(t_values.size, z) + if type(dz) == int or dz.size == 1: + # print('dz is contant') + # print(dz) + dz = np.full(t_values.size, dz) + + # print(type(dx), type(dy), type(dz)) + # print(type(x), type(y), type(z)) + + # Save rotation matrix for each time step + rot = [] + if align: + for i in range(len(t_values)): + dr = [dx[i], dy[i], dz[i]] + + # Calculate required rotation matrix from initial orientation. + x1 = [1, 0, 0] + rot.append(np.matrix(rotation_matrix_from_vectors(x1, dr))) + else: + for i in range(len(t_values)): + # Calculate required rotation matrix from initial orientation. + y1 = [0, 0, -1] + x1 = [1, 0, 0] + rot.append(np.matrix(rotation_matrix_from_vectors(x1, y1))) + + r = [x, y, z] + # dr = [dx, dy, dz] + print_JFH(t_values, r, rot, jfh_path) diff --git a/pyrpod/rpod/PlumeStrikeEstimationStudy.py b/pyrpod/rpod/PlumeStrikeEstimationStudy.py index f270e86..09de88f 100644 --- a/pyrpod/rpod/PlumeStrikeEstimationStudy.py +++ b/pyrpod/rpod/PlumeStrikeEstimationStudy.py @@ -1,1365 +1,1365 @@ -import numpy as np -import os -import math - -from stl import mesh -import matplotlib.pyplot as plt -from mpl_toolkits import mplot3d - -from pyrpod.vehicle.LogisticsModule import LogisticsModule -from pyrpod.mission.MissionPlanner import MissionPlanner -from pyrpod.plume.RarefiedPlumeGasKinetics import SimplifiedGasKinetics - -from pyrpod.util.io.file_print import print_1d_JFH -from pyrpod.util.io.fs import ensure_dir, resolve_asset_path -from pyrpod.util.stl.stl import load_stl, transform_mesh - -from tqdm import tqdm -from queue import Queue - -from pyrpod.logging_utils import get_logger -from pyrpod.util.math.transform import rotation_matrix_from_vectors - -# New modular imports for refactor -from pyrpod.rpod.approach_maneuvers import ( - ApproachInputs, - compute_1d_approach, -) -from pyrpod.rpod.io import ensure_results_dirs, write_jfh -from pyrpod.rpod.PlumeStudyExport import PlumeStudyExport -from pyrpod.plume.PlumeStrikeCalculator import ( - compute_face_centroids, - compute_plume_strikes, - extract_plume_params, - run_parallel_plume_strikes, -) - -logger = get_logger("pyrpod.rpod.PlumeStrikeEstimationStudy") - -class PlumeStrikeEstimationStudy (MissionPlanner): - """ - Class responsible for analyzing RPOD performance of visiting vehicles. - - Caculated metrics (outputs) include propellant usage, plume impingement, - trajectory character, and performance with respect to factors of safety. - - Data Inputs inlcude (redundant? better said in user guide?) - 1. LogisticsModule (LM) object with properly defined RCS configuration - 2. Jet Firing History including LM location and orientation with repsect to the Gateway. - 3. Selected plume models for impingement analysis. - 4. Surface mesh data for target and visiting vehicle. - - Attributes - ---------- - - vv : LogisticsModule - Visiting vehicle of interest. Includes complete RCS configuration and surface mesh data. - - jfh : JetFiringHistory - Includes VV location and orientation with respect to the TV. - - plume_model : PlumeModel - Contains the relevant governing equations selected for analysis. - - Methods - ------- - study_init(self, JetFiringHistory, Target, Vehicle) - Designates assets for RPOD analysis. - - graph_init_config(self) - Creates visualization data for initiial configuration of RPOD analysis. - - graph_jfh_thruster_check(self) - Creates visualization data for initiial configuration of RPOD analysis. - - graph_clusters(self, firing, vv_orientation) - Creates visualization data for the cluster. - - graph_jfh(self) - Creates visualization data for the trajectory of the proposed RPOD analysis. - - update_window_queue(self, window_queue, cur_window, firing_time, window_size) - Takes the most recent window of time size, and adds the new firing time to the sum, and the window_queue. - If the new window is larger than the allowed window_size, then earliest firing times are removed - from the queue and subtracted from the cur_window sum, until the sum fits within the window size. - A counter for how many firing times are removed and subtracted is recorded. - - update_parameter_queue(self, param_queue, param_window_sum, get_counter) - Takes the current parameter_queue, and removes the earliest tracked parameters from the front of the queue. - This occurs "get_counter" times. Each time a parameter is popped from the queue, the sum is also updated, - as to not track the removed parameter (ie. subtract the value) - - jfh_plume_strikes(self) - Calculates number of plume strikes according to data provided for RPOD analysis. - Method does not take any parameters but assumes that study assets are correctly configured. - These assets include one JetFiringHistory, one TargetVehicle, and one VisitingVehicle. - A Simple plume model is used. It does not calculate plume physics, only strikes. which - are determined with a user defined "plume cone" geometry. Simple vector mathematics is - used to determine if an VTK surface elements is struck by the "plume cone". - - print_jfh_1d_approach(v_ida, v_o, r_o) - Method creates JFH data for axial approach using simpified physics calculations. - """ - # def __init__(self): - # print("Initialized Approach Visualizer") - def study_init(self, JetFiringHistory, Target, Vehicle): - """ - Designates assets for RPOD analysis. - - Parameters - ---------- - JetFiringHistory : JetFiringHistory - Object thruster firing history. It includes VV position, orientation, and IDs for active thrusters. - - Target : TargetVehicle - Object containing surface mesh and thruster configurations for the Visiting Vehicle. - - Vehicle : VisitingVehicle - Object containing surface mesh and surfave properties for the Target Vehicle. - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - """ - self.jfh = JetFiringHistory - self.target = Target - self.vv = Vehicle - # visualization/export helper - self.viz = PlumeStudyExport(self.environment) - - def graph_init_config(self): - """ - Creates visualization data for initiial configuration of RPOD analysis. - - NOTE: Method does not take any parameters. It assumes that self.environment.case_dir - and self.environment.config are instatiated correctly. Potential defensive programming statements? - - TODO: Needs to be re-factored to save VTK data in proper case directory. - - Returns - ------- - Method doesn't currently return anything. Simply produces data as needed. - Does the method need to return a status message? or pass similar data? - """ - - # Save first coordinate in the JFH - vv_initial_firing = self.jfh.JFH[0] - # Log initial configuration details for debugging - logger.debug("Initial VV firing position: %s", vv_initial_firing['xyz']) - - # Translate VV STL to first coordinate - self.vv.mesh.translate(vv_initial_firing['xyz']) - - # Combine target and VV STLs into one "Mesh" object. - combined = mesh.Mesh(np.concatenate( - [self.target.mesh.data, self.vv.mesh.data] - )) - - figure = plt.figure() - # axes = mplot3d.Axes3D(figure) - axes = figure.add_subplot(projection='3d') - axes.add_collection3d(mplot3d.art3d.Poly3DCollection(combined.vectors)) - # axes.quiver(X, Y, Z, U, V, W, color=(0,0,0), length=1, normalize=True) - lim = 100 - axes.set_xlim([-1 * lim, lim]) - axes.set_ylim([-1 * lim, lim]) - axes.set_zlim([-1 * lim, lim]) - axes.set_xlabel('X') - axes.set_ylabel('Y') - axes.set_zlabel('Z') - # figure.suptitle(str(i)) - plt.show() - - - def graph_jfh_thruster_check(self): - """ - Creates visualization data for initiial configuration of RPOD analysis. - - NOTE: Method does not take any parameters. It assumes that self.environment.case_dir - and self.environment.config are instatiated correctly. Potential defensive programming statements? - - TODO: Needs to be re-factored to save VTK data in proper case directory. - - Returns - ------- - Method doesn't currently return anything. Simply produces data as needed. - Does the method need to return a status message? or pass similar data? - """ - - # Link JFH numbering of thrusters to thruster names. - link = {} - i = 1 - for thruster in self.vv.thruster_data: - link[str(i)] = self.vv.thruster_data[thruster]['name'] - i = i + 1 - - # Loop through each firing in the JFH. - for firing in range(len(self.jfh.JFH)): - - # Save active thrusters for current firing. - thrusters = self.jfh.JFH[firing]['thrusters'] - - # Load and graph STL of visting vehicle. - VVmesh = load_stl('../stl/cylinder.stl') - - figure = plt.figure() - # axes = mplot3d.Axes3D(figure) - axes = figure.add_subplot(projection = '3d') - axes.add_collection3d(mplot3d.art3d.Poly3DCollection(VVmesh.vectors)) - - # Load and graph STLs of active thrusters. - for thruster in thrusters: - # Map thruster ID - thruster_id = link[str(thruster)][0] - - # Load plume STL in initial configuration. - plumeMesh = load_stl('../stl/mold_funnel.stl') - - # Tranform plume into initial configuration. - # TODO: edit mold_funnel.stl to not require these transforms. - rot_mat = np.array([ - [1, 0, 0], - [0, -1, 0], - [0, 0, -1], - ]) - plumeMesh = transform_mesh( - plumeMesh, - rotation_matrix=rot_mat, - translation_vector=[0, 0, -50], - scale_factor=0.05 - ) - - # Transform plume according to thruster and VV configuration. - plumeMesh = transform_mesh( - plumeMesh, - rotation_matrix=np.array(self.vv.thruster_data[thruster_id]['dcm']).T, - translation_vector=self.vv.thruster_data[thruster_id]['exit'][0] - ) - - logger.debug("Thruster %s DCM: %s", thruster_id, self.vv.thruster_data[thruster_id]['dcm']) - - # Add surface to graph. - surface = mplot3d.art3d.Poly3DCollection(plumeMesh.vectors) - surface.set_facecolor('orange') - axes.add_collection3d(surface) - - lim = 7 - axes.set_xlim([-1*lim - 3, lim - 3]) - axes.set_ylim([-1*lim, lim]) - axes.set_zlim([-1*lim, lim]) - axes.set_xlabel('X') - axes.set_ylabel('Y') - axes.set_zlabel('Z') - shift=0 - axes.view_init(azim=0, elev=2*shift) - - - logger.debug("Completed plotting thruster check for firing %d", firing) - - if firing < 10: - index = '00' + str(firing) - elif firing < 100: - index = '0' + str(firing) - else: - index = str(i) - # delegate figure saving to export helper - self.viz.save_figure(figure, os.path.join(self.environment.case_dir, 'img', 'frame' + str(index) + '.png')) - - def graph_clusters(self, firing, vv_orientation): - """ - Creates visualization data for the cluster. - Parameters - ---------- - firing : int - Loop iterable over the length of the number of thrusters firing in the JFH. - - vv_orientation : np.array - DCM from the JFH. - Returns - ------- - active_clusters : mesh - Cluster of the current thruster firing. - """ - active_clusters = None - clusters_list = [] - for number in range(len(self.vv.cluster_data)): - cluster_name = 'P' + str(number + 1) - # print(cluster_name) - clusters_list.append(cluster_name) - - # print('clusters_list is', clusters_list) - # Load and graph STLs of active clusters. - for cluster in clusters_list: - - # Load plume STL in initial configuration. - clusterMesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_cluster'])) - - # Transform cluster - - # First, according to DCM of current cluster in CCF - cluster_orientation = np.array( - self.vv.cluster_data[cluster]['dcm'] - ) - clusterMesh.rotate_using_matrix(cluster_orientation.transpose()) - - # Second, according to DCM of VV in JFH - clusterMesh.rotate_using_matrix(vv_orientation.transpose()) - - # Third, according to position vector of the VV in JFH - clusterMesh.translate(self.jfh.JFH[firing]['xyz']) - - # Fourth, according to position of current cluster in CCF - clusterMesh.translate(self.vv.cluster_data[cluster]['exit'][0]) - # print(self.vv.cluster_data[cluster]['exit'][0]) - - if active_clusters == None: - active_clusters = clusterMesh - else: - active_clusters = mesh.Mesh( - np.concatenate([active_clusters.data, clusterMesh.data]) - ) - return active_clusters - - def graph_jfh(self, trade_study = False): - """ - Creates visualization data for the trajectory of the proposed RPOD analysis. - - This method does NOT calculate plume strikes. - - This utilities allows engineers to visualize the trajectory in the JFH before running - the full simulation and wasting computation time. - Returns - ------- - Method doesn't currently return anything. Simply produces data as needed. - Does the method need to return a status message? or pass similar data? - """ - # Link JFH numbering of thrusters to thruster names. - link = {} - i = 1 - for thruster in self.vv.thruster_data: - link[str(i)] = self.vv.thruster_data[thruster]['name'] - i = i + 1 - - # Create results directory if it doesn't already exist. - results_dir = self.environment.case_dir + 'results' - if not os.path.isdir(results_dir): - # print("results dir doesn't exist") - os.mkdir(results_dir) - - - if not trade_study: - results_dir = results_dir + "/jfh" - if not os.path.isdir(results_dir): - #print("results dir doesn't exist") - os.mkdir(results_dir) - - if trade_study: - v_o = ['vo_0', 'vo_1', 'vo_2', 'vo_3', 'vo_4'] - cants = ['cant_0', 'cant_1', 'cant_2', 'cant_3', 'cant_4'] - for v in v_o: - for cant in cants: - results_dir_case = results_dir + "/" + v + '_' + cant - if not os.path.isdir(results_dir_case): - #print("results dir doesn't exist") - os.mkdir(results_dir_case) - - # Save STL surface of target vehicle to local variable. - target = self.target.mesh - - # Loop through each firing in the JFH. - for firing in range(len(self.jfh.JFH)): - # print('firing =', firing+1) - - # Save active thrusters for current firing. - thrusters = self.jfh.JFH[firing]['thrusters'] - # print("thrusters", thrusters) - - # Load, transform, and, graph STLs of visiting vehicle. - VVmesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_lm'])) - vv_orientation = np.array(self.jfh.JFH[firing]['dcm']) - # print(vv_orientation.transpose()) - VVmesh.rotate_using_matrix(vv_orientation.transpose()) - VVmesh.translate(self.jfh.JFH[firing]['xyz']) - - active_cones = None - - # Load and graph STLs of active clusters. - if self.vv.use_clusters == True: - active_clusters = self.graph_clusters(firing, vv_orientation) - - # Load and graph STLs of active thrusters. - for thruster in thrusters: - - - # Save thruster id using indexed thruster value. - # Could naming/code be more clear? - # print('thruster num', thruster, 'thruster id', link[str(thruster)][0]) - thruster_id = link[str(thruster)][0] - - # Load plume STL in initial configuration. - plumeMesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_thruster'])) - - # Transform plume - - # First, according to DCM of current thruster id in TCF - thruster_orientation = np.array( - self.vv.thruster_data[thruster_id]['dcm'] - ) - plumeMesh.rotate_using_matrix(thruster_orientation.transpose()) - - # Second, according to DCM of VV in JFH - plumeMesh.rotate_using_matrix(vv_orientation.transpose()) - - # Third, according to position vector of the VV in JFH - plumeMesh.translate(self.jfh.JFH[firing]['xyz']) - - # Fourth, according to position of current cluster in CCF - if self.vv.use_clusters == True: - # thruster_id[0] = "P" and thruster_id[1] = "#", adding these gives the cluster identifier - plumeMesh.translate(self.vv.cluster_data[thruster_id[0] + thruster_id[1]]['exit'][0]) - - # Fifth, according to exit vector of current thruster id in TCD - plumeMesh.translate(self.vv.thruster_data[thruster_id]['exit'][0]) - - # Takeaway: Do rotations before translating away from the rotation axes! - - - if active_cones == None: - active_cones = plumeMesh - else: - active_cones = mesh.Mesh( - np.concatenate([active_cones.data, plumeMesh.data]) - ) - - # print('DCM: ', self.vv.thruster_data[thruster_id]['dcm']) - # print('DCM: ', thruster_orientation[0], thruster_orientation[1], thruster_orientation[2]) - - if self.vv.use_clusters != True: - if not active_cones == None: - VVmesh = mesh.Mesh( - np.concatenate([VVmesh.data, active_cones.data]) - ) - if self.vv.use_clusters == True: - if not active_cones == None: - VVmesh = mesh.Mesh( - np.concatenate([VVmesh.data, active_cones.data, active_clusters.data]) - ) - - # print(self.vv.mesh) - - # print(self.environment.case_dir + self.environment.config['stl']['vv']) - - if trade_study == False: - path_to_stl = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{firing}.stl") - elif trade_study == True: - path_to_stl = os.path.join(self.environment.case_dir, "results", self.get_case_key(), "jfh", f"firing-{firing}.stl") - - # self.vv.convert_stl_to_vtk(path_to_vtk, mesh =VVmesh) - self.viz.export_firing(VVmesh, path_to_stl) - - def visualize_sweep(self, config_iter): - """ - Creates visualization data for the trajectory of the proposed RPOD analysis. - - This method is valid for SINGLE JFH firings, due to file naming conventions. - Numbering of files is based on the iteration number of the current configuration provided. - - Method used for mdao_unit_test_02.py - - This utility allows engineers to visualize a configuration sweep before running - the full simulation and wasting computational resources. - Parameters - ---------- - None - - Returns - ------- - Method doesn't currently return anything. Simply produces data as needed. - Does the method need to return a status message? or pass similar data? - """ - # Link JFH numbering of thrusters to thruster names. - link = {} - i = 1 - for thruster in self.vv.thruster_data: - link[str(i)] = self.vv.thruster_data[thruster]['name'] - i = i + 1 - # print('link is', link) - - # Create results directory if it doesn't already exist. - results_dir = self.environment.case_dir + 'results' - if not os.path.isdir(results_dir): - # print("results dir doesn't exist") - os.mkdir(results_dir) - - results_dir = results_dir + "/jfh" - if not os.path.isdir(results_dir): - # print("results dir doesn't exist") - os.mkdir(results_dir) - - # Save STL surface of target vehicle to local variable. - target = self.target.mesh - - # Loop through each firing in the JFH. - for firing in range(len(self.jfh.JFH)): - # print('firing =', firing+1) - - # Save active thrusters for current firing. - thrusters = self.jfh.JFH[firing]['thrusters'] - # print("thrusters is", thrusters) - - # Load, transform, and, graph STLs of visiting vehicle. - VVmesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_lm'])) - vv_orientation = np.array(self.jfh.JFH[firing]['dcm']) - # print(vv_orientation.transpose()) - VVmesh.rotate_using_matrix(vv_orientation.transpose()) - VVmesh.translate(self.jfh.JFH[firing]['xyz']) - - active_cones = None - - # Load and graph STLs of active clusters. - if self.vv.use_clusters == True: - active_clusters = self.graph_clusters(firing, vv_orientation) - - # Load and graph STLs of active thrusters. - for thruster in thrusters: - - thruster_id = link[str(thruster)][0] - - # Save thruster id using indexed thruster value. - # Could naming/code be more clear? - # print('thruster num', thruster, 'thruster id', link[str(thruster)][0]) - - # Load plume STL in initial configuration. - plumeMesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_thruster'])) - - # Transform plume - - # First, according to DCM of current thruster id in TCF - thruster_orientation = np.array( - self.vv.thruster_data[thruster_id]['dcm'] - ) - plumeMesh.rotate_using_matrix(thruster_orientation.transpose()) - - # Second, according to DCM of VV in JFH - plumeMesh.rotate_using_matrix(vv_orientation.transpose()) - - # Third, according to position vector of the VV in JFH - plumeMesh.translate(self.jfh.JFH[firing]['xyz']) - - # Fourth, according to position of current cluster in CCF - if self.vv.use_clusters == True: - # thruster_id[0] = "P" and thruster_id[1] = "#", adding these gives the cluster identifier - plumeMesh.translate(self.vv.cluster_data[thruster_id[0] + thruster_id[1]]['exit'][0]) - - # Fifth, according to exit vector of current thruster id in TCD - plumeMesh.translate(self.vv.thruster_data[thruster_id]['exit'][0]) - - # Takeaway: Do rotations before translating away from the rotation axes! - - - if active_cones == None: - active_cones = plumeMesh - else: - active_cones = mesh.Mesh( - np.concatenate([active_cones.data, plumeMesh.data]) - ) - - # print('DCM: ', self.vv.thruster_data[thruster_id]['dcm']) - # print('DCM: ', thruster_orientation[0], thruster_orientation[1], thruster_orientation[2]) - - if self.vv.use_clusters != True: - if not active_cones == None: - VVmesh = mesh.Mesh( - np.concatenate([VVmesh.data, active_cones.data]) - ) - if self.vv.use_clusters == True: - if not active_cones == None: - VVmesh = mesh.Mesh( - np.concatenate([VVmesh.data, active_cones.data, active_clusters.data]) - ) - - # print(self.vv.mesh) - - # print(self.environment.case_dir + self.environment.config['stl']['vv']) - - if self.count > 0: - path_to_vtk = os.path.join(self.environment.case_dir, "results", "strikes", f"firing-{self.count}-{firing}") - path_to_stl = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{self.count}-{firing}.stl") - else: - path_to_vtk = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{firing}") - path_to_stl = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{firing}.stl") - # self.vv.convert_stl_to_vtk(path_to_vtk, mesh =VVmesh) - self.viz.export_firing(VVmesh, path_to_stl) - - # def update_window_queue(self, window_queue, cur_window, firing_time, window_size): - # """ - # Takes the most recent window of time size, and adds the new firing time to the sum, and the window_queue. - # If the new window is larger than the allowed window_size, then earliest firing times are removed - # from the queue and subtracted from the cur_window sum, until the sum fits within the window size. - # A counter for how many firing times are removed and subtracted is recorded. - - # Parameters - # ---------- - # window_queue : Queue - # Queue holding the tracked firing times - # cur_window : float - # sum of the tracked firing times (s) - # firing_time : float - # length of current firing (s) - # window_size : float - # max length of a window of time to track (s) - - # Returns - # ------- - # Queue - # Stores tracked firing times after update - # float - # sum of tracked firing times after update - # int - # number of firings removed from the queue - # """ - # window_queue.put(firing_time) - # cur_window += firing_time - # get_counter = 0 - # while cur_window > window_size: - # old_firing_time = window_queue.get() - # cur_window -= old_firing_time - # get_counter +=1 - # return window_queue, cur_window, get_counter - - # def update_parameter_queue(self, param_queue, param_window_sum, get_counter): - # """ - # Takes the current parameter_queue, and removes the earliest tracked parameters from the front of the queue. - # This occurs "get_counter" times. Each time a parameter is popped from the queue, the sum is also updated, - # as to not track the removed parameter (ie. subtract the value) - - # Parameters - # ---------- - # param_queue : Queue - # Queue holding the tracked parameters per firing - # param_window_sum : float - # sum of the parameters in all the tracked firings - # get_counter : int - # number of times to remove a tracked parameter from the front of the queue - - # Returns - # ------- - # Queue - # stores tracked paramters per firing after update - # float - # sum of tracked parameter after update - # """ - # for i in range(get_counter): - # old_param = param_queue.get() - # param_window_sum -= old_param - # return param_queue, param_window_sum - - # Helper functions for jfh_plume_strikes - def create_results_dir(self): - """ - Creates a results directory and sub-directories if they don't already exist. - """ - sub_dirs = ['results', 'results/strikes', 'results/jfh'] - for sub_dir in sub_dirs: - ensure_dir(os.path.join(self.environment.case_dir, sub_dir)) - - def set_strike_fields(self, target): - # Initiate array containing cummulative strikes. - cum_strikes = np.zeros(len(target.vectors)) - - # using plume physics? - if self.environment.config['pm']['kinetics'] != 'None': - - # Initiate array containing max pressures induced on each element. - max_pressures = np.zeros(len(target.vectors)) - - # Initiate array containing max shears induced on each element. - max_shears = np.zeros(len(target.vectors)) - - # Initiate array containing cummulative heatflux. - cum_heat_flux_load = np.zeros(len(target.vectors)) - - return cum_strikes, max_pressures, max_shears, cum_heat_flux_load - - return cum_strikes - - def extract_firing_data(self, firing): - # Save active thrusters for current firing. - thrusters = self.jfh.JFH[firing]['thrusters'] - # print("thrusters", thrusters) - - # Load visiting vehicle position and orientation - vv_pos = self.jfh.JFH[firing]['xyz'] - - vv_orientation = np.array(self.jfh.JFH[firing]['dcm']).transpose() - - return thrusters, vv_pos, vv_orientation - - def set_plume_strike_fields(self, target): - # reset strikes for each firing - strikes = np.zeros(len(target.vectors)) - - if self.environment.config['pm']['kinetics'] != 'None': - # reset pressures for each firing - pressures = np.zeros(len(target.vectors)) - - # reset shear pressures for each firing - shear_stresses = np.zeros(len(target.vectors)) - - # reset heat fluxes for each firing - heat_flux = np.zeros(len(target.vectors)) - heat_flux_load = np.zeros(len(target.vectors)) - return strikes, pressures, shear_stresses, heat_flux, heat_flux_load - else: - return strikes - - def set_plume_transformations(self, thruster_id, vv_orientation, vv_pos): - # Load data to calculate plume transformations - - # First, according to DCM and exit vector using current thruster id in TCD - thruster_orientation = np.array( - self.vv.thruster_data[thruster_id]['dcm'] - ).transpose() - - thruster_orientation = thruster_orientation.dot(vv_orientation) - # print('DCM: ', self.vv.thruster_data[thruster_id]['dcm']) - # print('DCM: ', thruster_orientation[0], thruster_orientation[1], thruster_orientation[2]) - plume_normal = np.array(thruster_orientation[0]) - # print("plume normal: ", plume_normal) - - # calculate thruster exit coordinate with respect to the Target Vehicle. - - # print(self.vv.thruster_data[thruster_id]) - thruster_pos = vv_pos + np.array(self.vv.thruster_data[thruster_id]['exit']) - thruster_pos = thruster_pos[0] - # print('thruster position', thruster_pos) - - return plume_normal, thruster_pos, thruster_orientation - - def set_face_centroid(self, face): - # Calculate centroid for face - - x = np.array(face[0]).mean() - y = np.array(face[1]).mean() - z = np.array(face[2]).mean() - - centroid = np.array([x, y, z]) - - return centroid - - def set_face_distance(self, thruster_pos, centroid): - # Calculate distance vector between face centroid and thruster exit. - distance = thruster_pos - centroid - # print('distance vector', distance) - norm_distance = np.sqrt(distance[0]**2 + distance[1]**2 + distance[2]**2) - - unit_distance = distance / norm_distance - # print('distance magnitude', norm_distance) - - return distance, norm_distance, unit_distance - - def _resolve_parallel_options(self, parallel, workers, n_firings): - """ - Resolves parallel execution settings for jfh_plume_strikes(). - - Precedence: explicit method arguments override the optional - [exec] config section, which defaults to serial execution. - - Config keys (both optional): - - [exec] parallel : bool — enable process-based parallelization - across firings (default false). - - [exec] workers : int — number of worker processes. Defaults to - min(os.cpu_count(), n_firings) when parallel is enabled. - - Returns - ------- - (bool, int) - (parallel_enabled, workers) — workers is capped at n_firings; - workers <= 1 resolves to serial execution. - """ - config = self.environment.config - if parallel is None: - try: - parallel = config.getboolean('exec', 'parallel', fallback=False) - except ValueError as exc: - raise ValueError( - "Invalid config value for [exec] parallel: expected a " - "boolean (true/false)." - ) from exc - if workers is None: - try: - workers = config.getint('exec', 'workers', fallback=None) - except ValueError as exc: - raise ValueError( - "Invalid config value for [exec] workers: expected a " - "positive integer." - ) from exc - if workers is not None and workers < 1: - raise ValueError( - f"workers must be a positive integer, got {workers}." - ) - - if not parallel: - return False, 1 - - if workers is None: - workers = min(os.cpu_count() or 1, n_firings) - # Never spawn more workers than there are firings to compute. - workers = min(workers, n_firings) - if workers <= 1: - return False, 1 - return True, workers - - def jfh_plume_strikes(self, parallel=None, workers=None): - """ - Calculates number of plume strikes according to data provided for RPOD analysis. - Method assumes that study assets are correctly configured. - These assets include one JetFiringHistory, one TargetVehicle, and one VisitingVehicle. - A Simple plume model is used. It does not calculate plume physics, only strikes. which - are determined with a user defined "plume cone" geometry. Simple vector mathematics is - used to determine if an VTK surface elements is struck by the "plume cone". - - Parameters - ---------- - parallel : bool, optional - Enable process-based parallelization across firings. Defaults - to None, meaning "use the optional [exec] parallel config key", - which itself defaults to false (serial, legacy behavior). - workers : int, optional - Number of worker processes when parallel execution is enabled. - Defaults to None, meaning "use the optional [exec] workers - config key", falling back to min(os.cpu_count(), n_firings). - - Notes - ----- - Only independent per-firing strike arrays are computed in worker - processes; cumulative arrays (cum_strikes, max_pressures, - max_shears, cum_heat_flux_load) are accumulated serially in - original firing order, and VTK output is written serially by the - parent process. If the parallel path fails to initialize or run, - the method logs a warning and falls back to serial execution. - - Returns - ------- - dict - firing_data keyed by firing number ('1'..'N'), each holding - per-face arrays: strikes, cum_strikes, and, when kinetics is - enabled, pressures, max_pressures, shear_stress, max_shears, - heat_flux_rate, heat_flux_load, cum_heat_flux_load. - """ - # Prepare results directories and target data - self.create_results_dir() - target = self.target.mesh - target_normals = target.get_unit_normals() - # The target is stationary for the whole run, so face centroids are - # computed once here and reused for every firing. - target_centroids = compute_face_centroids(target.vectors) - - # Initialize cumulative arrays - kinetics_on = self.environment.config['pm']['kinetics'] != 'None' - if kinetics_on: - cum_strikes, max_pressures, max_shears, cum_heat_flux_load = self.set_strike_fields(target) - else: - cum_strikes = self.set_strike_fields(target) - - firing_data = {} - - n_firings = len(self.jfh.JFH) - - # Build serializable step dicts once; shared by serial and parallel paths. - steps = [] - for firing in range(n_firings): - steps.append({ - 'thrusters': self.jfh.JFH[firing]['thrusters'], - 'xyz': np.array(self.jfh.JFH[firing]['xyz']), - 'dcm': np.array(self.jfh.JFH[firing]['dcm']), - 't': float(self.jfh.JFH[firing]['t']) - }) - - parallel_enabled, n_workers = self._resolve_parallel_options(parallel, workers, n_firings) - - # Optionally compute independent per-firing results in worker - # processes. Workers receive only plain serializable inputs (arrays, - # dicts, config scalars) — never the study/vehicle/environment objects. - per_firing_results = None - if parallel_enabled: - try: - per_firing_results = run_parallel_plume_strikes( - jfh_steps=steps, - face_centroids=target_centroids, - target_unit_normals=target_normals, - thruster_data=self.vv.thruster_data, - thruster_metrics=getattr(self.vv, 'thruster_metrics', None), - plume_params=extract_plume_params(self.environment), - workers=n_workers, - ) - except Exception as exc: - logger.warning( - "Parallel plume strike execution failed (%s: %s); " - "falling back to serial execution.", - type(exc).__name__, exc, - ) - per_firing_results = None - - # Loop through each firing in the JFH and delegate to impingement module - for firing in range(n_firings): - step = steps[firing] - - if per_firing_results is not None: - result = per_firing_results[firing] - else: - result = compute_plume_strikes( - target_mesh=target, - target_unit_normals=target_normals, - vv=self.vv, - jfh_step=step, - environment=self.environment, - face_centroids=target_centroids, - ) - - strikes = result["strikes"] - cum_strikes = cum_strikes + strikes - - cellData = { - "strikes": strikes, - "cum_strikes": cum_strikes.copy(), - } - - if kinetics_on: - pressures = result.get("pressures") - shear_stresses = result.get("shear_stress") - heat_flux_rate = result.get("heat_flux_rate") - heat_flux_load = result.get("heat_flux_load") - - max_pressures = np.maximum(max_pressures, pressures) - max_shears = np.maximum(max_shears, shear_stresses) - cum_heat_flux_load = cum_heat_flux_load + heat_flux_load - - cellData.update({ - "pressures": pressures, - "max_pressures": max_pressures, - "shear_stress": shear_stresses, - "max_shears": max_shears, - "heat_flux_rate": heat_flux_rate, - "heat_flux_load": heat_flux_load, - "cum_heat_flux_load": cum_heat_flux_load, - }) - - firing_data[str(firing+1)] = cellData - - # if checking constraints: - # save all new pressure and heat flux values into each cell's respective queue - # add pressure and heat_flux into each cell's window sum - # update the window queues based on their max sizes, and the firing times - # update the parameter queues based on the updates made on the window queues - # if self.environment.config['pm']['kinetics'] != 'None' and checking_constraints: - # for i in range(len(pressures)): - # pressure_queues[i].put(float(pressures[i])) - # pressure_window_sums[i] += pressures[i] - - # heat_flux_queues[i].put(float(heat_flux_load[i])) - # heat_flux_window_sums[i] += heat_flux_load[i] - - # pressure_window_queue, pressure_cur_window, pressure_get_counter = self.update_window_queue( - # pressure_window_queue, pressure_cur_window, firing_time, pressure_window_size) - - # heat_flux_window_queue, heat_flux_cur_window, heat_flux_get_counter = self.update_window_queue( - # heat_flux_window_queue, heat_flux_cur_window, firing_time, heat_flux_window_size) - - # for queue_index in range(len(pressure_queues)): - # if not pressure_queues[queue_index].empty(): - # pressure_queues[queue_index], pressure_window_sums[queue_index] = self.update_parameter_queue(pressure_queues[queue_index], pressure_window_sums[queue_index], pressure_get_counter) - - # if not heat_flux_queues[queue_index].empty() and heat_flux_window_sums[queue_index] != 0: - # heat_flux_queues[queue_index], heat_flux_window_sums[queue_index] = self.update_parameter_queue(heat_flux_queues[queue_index], heat_flux_window_sums[queue_index], heat_flux_get_counter) - - # #check if instantaneous constraints are broken - # if pressures[queue_index] > pressure_constraint and not failed_constraints: - # constraint_file.write(f"Pressure constraint failed at cell #{i}.\n") - # constraint_file.write(f"Pressure reached {pressures[queue_index]}.\n\n") - # failed_constraints = 1 - # if shear_stresses[queue_index] > shear_constraint and not failed_constraints: - # constraint_file.write(f"Shear constraint failed at cell #{i}.\n") - # constraint_file.write(f"Shear reached {shear_stresses[queue_index]}.\n\n") - # failed_constraints = 1 - # if heat_flux[queue_index] > heat_flux_constraint and not failed_constraints: - # constraint_file.write(f"Heat flux constraint failed at cell #{i}.\n") - # constraint_file.write(f"Heat flux reached {heat_flux[queue_index]}.\n\n") - # failed_constraints = 1 - - # # check if window constraints are broken, and report - # if pressure_window_sums[queue_index] > pressure_window_constraint and not failed_constraints: - # constraint_file.write(f"Pressure window constraint failed at cell #{queue_index}.\n") - # constraint_file.write(f"Pressure winodw reached {pressure_window_sums[queue_index]}.\n\n") - # failed_constraints = 1 - # if heat_flux_window_sums[queue_index] > heat_flux_window_constraint and not failed_constraints: - # constraint_file.write(f"Heat flux window constraint failed at cell #{queue_index}.\n") - # constraint_file.write(f"Heat flux load reached {heat_flux_window_sums[queue_index]}.\n\n") - # failed_constraints = 1 - - - path_to_vtk = self.environment.case_dir + "results/strikes/firing-" + str(firing) - - # print(cellData) - # input() - self.target.convert_stl_to_vtk_strikes(path_to_vtk, cellData.copy(), target) - - # if self.environment.config['pm']['kinetics'] != 'None' and checking_constraints: - # if not failed_constraints: - # constraint_file.write(f"All impingement constraints met.") - # # constraint_file.close() - - return firing_data - - def calc_time_multiplier(self, v_ida, v_o, r_o): - # Determine thruster configuration characterstics. - # The JFH only contains firings done by the neg_x group - m_dot_sum = self.calc_m_dot_sum('neg_x') - # print('m_dot_sum is', m_dot_sum) - MIB = self.vv.thruster_metrics[self.vv.thruster_data[self.vv.rcs_groups['neg_x'][0]]['type'][0]]['MIB'] - # print('MIB is', MIB) - F_thruster = self.vv.thruster_metrics[self.vv.thruster_data[self.vv.rcs_groups['neg_x'][0]]['type'][0]]['F'] - F = F_thruster * np.cos(self.vv.decel_cant) - n_thrusters = len(self.vv.rcs_groups['neg_x']) - F = F * n_thrusters - - # Defining a multiplier reduce time steps and make running faster - dt = (MIB / F_thruster) - # print('dt is', dt) - dm_firing = m_dot_sum * dt - # print('dm_firing is', dm_firing) - docking_mass = self.vv.mass - # print('docking mass is', docking_mass) - - # Calculate required change in velocity. - dv_req = v_o - v_ida - v_e = self.calc_v_e('neg_x') - - # Calculate propellant used for docking and changes in mass. - forward_propagation = False - delta_mass_jfh = self.calc_delta_mass_v_e(dv_req, v_e, forward_propagation) - # print('delta_mass_docking is',delta_mass_jfh) - - pre_approach_mass = self.vv.mass - # print('pre-approach mass is', pre_approach_mass) - - # Instantiate data structure to hold JFH data + physics data. - # Initializing position - x = [r_o] - y = [0] - z = [0] - - # Initializing empty tracking lists - dx = [0] - t = [0] - dv = [0] - - # Initializing inertial state - dxdt = [v_o] - - # Initializing initial mass - mass = [pre_approach_mass] - - # Initializing list to later sum propellant expenditure - dm_total = [dm_firing] - - # Firing number - n = [1] - - # Create dummy rotation matrices. - x1 = [1, 0, 0] - y1 = [1, 0, 0] - - rot = [np.array(rotation_matrix_from_vectors(x1, y1))] - - # Calculate JFH and 1D physics data for required firings. - while (dv_req > 0): - # print('dv_req', round(dv_req, 4), 'n firings', n[i]) - - # Grab last value in the JFH arrays (initial conditions for current time step) - # print('x, dx, dt, t, dxdt, mass, dm_total') - # print(x[-1], dx[-1], dt_vals[-1], t[-1], dxdt[-1], mass[-1], dm_total[-1]) - - # Update VV mass per firing - mass_o = mass[-1] - mass.append(mass_o - dm_firing) - mass_f = mass[-1] - # print('mass_f is', mass_f) - - # Calculate velocity change per firing. - dv_firing = self.calc_delta_v(dt, v_e, m_dot_sum, mass_o) - dv.append(dv_firing) - # print('dv is', dv_firing) - # print(round(dxdt[-1] - dv_firing, 2)) - # input() - dxdt.append(dxdt[-1] - dv_firing) - - # Calculate distance traveled per firing - # print(dxdt[-1], dxdt[-2]) # last and second to last element. - v_avg = 0.5 * (dxdt[-1] + dxdt[-2]) - dx.append(v_avg * dt) - x.append(x[-1] - v_avg*dt) - y.append(0) - z.append(0) - - # Calculate left over v_req (TERMINATES LOOP) - dv_req -= dv_firing - - # Calculate mass expended up to this point. - dm_total.append(dm_total[-1] + dm_firing) - - # Calculate current firing. - n.append(n[-1]+1) - - # Add time data. - t.append(t[-1] + dt) - - rot.append(np.array(rotation_matrix_from_vectors(x1, y1))) - - # After simulating, estimate a coarse time multiplier based on number of firings - n_firings = len(t) - time_multiplier = 10 / n_firings if n_firings > 0 else 1 - # print('n firings:', len(t), 'time multiplier:', time_multiplier) - return (1 / time_multiplier) - - # one_d_results = { - # 'n_firings': n, - # 'x': x, - # 'dx': dx, - # 't': t, - # 'dv': dv, - # 'v': dxdt, - # 'mass': mass, - # 'delta_mass': dm_total - # } - - # print(one_d_results) - - def print_jfh_1d_approach_n_fire(self, v_ida, v_o, r_o, n_firings, trade_study = False): - # Delegate to approach_maneuvers.compute_1d_approach and rpod.io.write_jfh - tm = self.calc_time_multiplier(v_ida, v_o, r_o) - inputs = ApproachInputs(v_ida=float(v_ida), v_o=float(v_o), r_o=float(r_o), group='neg_x') - # Adapter for grouping methods if not explicitly available as a module - class _GroupingAdapter: - def __init__(self, outer): - self._outer = outer - def calc_m_dot_sum(self, group): - return self._outer.calc_m_dot_sum(group) - def calc_v_e(self, group): - return self._outer.calc_v_e(group) - - results = compute_1d_approach( - inputs=inputs, - vv=self.vv, - fuel_mgr=self, - grouping=_GroupingAdapter(self), - cant_rad=self.vv.decel_cant, - dt_strategy={"multiplier": tm}, - ) - - r = [results["x"], results["y"], results["z"]] - t_values = results["t"] - rot = results["rot"] - - # Build output path as before - if not trade_study: - jfh_path = self.environment.case_dir + 'jfh/' + self.environment.config['jfh']['jfh'] - else: - jfh_path = self.environment.case_dir + 'jfh/' + self.get_case_key() + '.A' - - os.makedirs(os.path.dirname(jfh_path), exist_ok=True) - write_jfh(t_values, r, rot, jfh_path, mode="1d") - - - def print_jfh_1d_approach(self, v_ida, v_o, r_o, trade_study = False): - """ - Method creates JFH data for axial approach using simpified physics calculations. - - This approach models one continuous firing. - - Kinematics and mass changes are discretized according to the thruster's minimum firing time. - - Parameters - ---------- - v_ida : float - VisitingVehicle docking velocity (determined by international docking adapter) - - v_o : float - VisitingVehicle incoming axial velocity. - - r_o : float - Initial distance to docking port. - - Returns - ------- - Method doesn't currently return anything. Simply prints data to a files as needed. - Does the method need to return a status message? or pass similar data? - - """ - # Delegate to approach_maneuvers with a fixed multiplier similar to legacy - inputs = ApproachInputs(v_ida=float(v_ida), v_o=float(v_o), r_o=float(r_o), group='neg_x') - class _GroupingAdapter: - def __init__(self, outer): - self._outer = outer - def calc_m_dot_sum(self, group): - return self._outer.calc_m_dot_sum(group) - def calc_v_e(self, group): - return self._outer.calc_v_e(group) - - results = compute_1d_approach( - inputs=inputs, - vv=self.vv, - fuel_mgr=self, - grouping=_GroupingAdapter(self), - cant_rad=self.vv.decel_cant, - dt_strategy={"multiplier": 60.0}, - ) - - r = [results["x"], results["y"], results["z"]] - t_values = results["t"] - rot = results["rot"] - - if not trade_study: - jfh_path = self.environment.case_dir + 'jfh/' + self.environment.config['jfh']['jfh'] - else: - jfh_path = self.environment.case_dir + 'jfh/' + self.get_case_key() + '.A' - - os.makedirs(os.path.dirname(jfh_path), exist_ok=True) - write_jfh(t_values, r, rot, jfh_path, mode="1d") - return - - def edit_1d_JFH(self, t_values, r, rot): - """ - Helper function to RPOD.calc_jfh_1d_approach() that is responsible for - modifying the JFH attribute in memory with the values calculated. - - Parameters - ---------- - t_values : np.array - Array containing time step data for each firing in the JFH. - - r : np.array - Array containing positional data for each firing in the JFH. - - rot : np.array - Array containing rotational data for each firing in the JFH. - - Returns - ------- - Method doesn't currently return anything. Simply prints data to a files as needed. - Does the method need to return a status message? or pass similar data? - - """ - # Printing out the empty JFH and checking its size - # print('self.jfh.JFH is', self.jfh.JFH) - # print('len(self.jfh.JFH) is', len(self.jfh.JFH)) - - # Scrap - # print('self.jfh.JFH[0] is', self.jfh.JFH[0]) - # print('self.jfh.JFH[1] is', self.jfh.JFH[1]) - # self.jfh.JFH.append({'nt': '1', 'dt': '0.000', 't': '1', 'dcm': [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], 'xyz': [-10.0, 0.0, 0.0], 'uf': 1.0, 'thrusters': [1, 2, 5, 6, 9, 10, 13, 14]}) - # print('self.jfh.JFH[0] is', self.jfh.JFH[0]) - - # Checking the time step - # print('t_values is', t_values) - # print('len(t_values) is', len(t_values)) - - # Changing the number of firings to be evaluated - self.jfh.nt = len(t_values) - # print('self.jfh.nt is', self.jfh.nt) - # print('self.jfh.nt after is', self.jfh.nt) - # print("self.jfh.JFH[0]['nt'] is", self.jfh.JFH[0]['nt']) - - # Confirming the time step is correct - # print('str(t_values[1]) is', str(t_values[1])) - - # Verifying the format of the rot array - # print('rot[0] is', rot[0]) - # print('[list(rot[0][0]), list(rot[0][1]), list(rot[0][2])] is', [list(rot[0][0]), list(rot[0][1]), list(rot[0][2])]) - # print('[list(rot[1][0]), list(rot[1][1]), list(rot[1][2])] is', [list(rot[1][0]), list(rot[1][1]), list(rot[1][2])]) - - # Verifying the format of the r array - # print('r is', r) - # print('[r[0][0], r[1][0], r[2][0]] is', [r[0][0], r[1][0], r[2][0]]) - # print('[r[0][1], r[1][1], r[2][1]] is', [r[0][1], r[1][1], r[2][1]]) - - for i in range(len(t_values)): - # NOTE: 'thrusters' is currently hardcoded - # NOTE: all the 'xyz' values are still negative - # Start from the required distance to slow down and approach zero - # print('-r[0][-1] + r[0][i] is', -r[0][-1] + r[0][i]) - - - # NOTE: The thrusters are currently hardcoded, but should be changed to the thrusters indicated in the neg_x group in the tgf - self.jfh.JFH.append({'nt': str(i + 1), 'dt': str(t_values[i]), 't': str(t_values[1]), 'dcm': [list(rot[i][0]), list(rot[i][1]), list(rot[i][2])], 'xyz': [-r[0][-1] + r[0][i] - 0.5, -r[1][i], -r[2][i]], 'uf': 1.0, 'thrusters': [1, 2, 5, 6, 9, 10, 13, 14]}) - - # Printing out the populated JFH and checking its size - # print('self.jfh.JFH is', self.jfh.JFH) - # print('len(self.jfh.JFH) is', len(self.jfh.JFH)) - - def calc_jfh_1d_approach(self, v_ida, v_o, cant): - """ - The x-position represents the distance required to reach a velocity of zero. - - Method calculates JFH data for 1D approach using simpified physics calculations. - - This approach models one continuous firing. - - Kinematics and mass changes are discretized according to the thruster's minimum firing time. - - Parameters - ---------- - v_ida : float - VisitingVehicle docking velocity (determined by international docking adapter) - - v_o : float - VisitingVehicle incoming axial velocity. - - cant : float - Angling of all deceleration thrusters in degrees that - - Returns - ------- - Method doesn't currently return anything. Simply prints data to a files as needed. - Does the method need to return a status message? or pass similar data? - - """ - # Delegate to compute-first API, then update in-memory JFH via existing helper - MissionPlanner.cant = np.radians(cant) - inputs = ApproachInputs(v_ida=float(v_ida), v_o=float(v_o), r_o=0.0, group='neg_x') - - class _GroupingAdapter: - def __init__(self, outer): - self._outer = outer - def calc_m_dot_sum(self, group): - return self._outer.calc_m_dot_sum(group) - def calc_v_e(self, group): - return self._outer.calc_v_e(group) - - results = compute_1d_approach( - inputs=inputs, - vv=self.vv, - fuel_mgr=self, - grouping=_GroupingAdapter(self), - cant_rad=MissionPlanner.cant, - dt_strategy={"multiplier": 100.0}, - ) - - t_values = results["t"] - r = [results["x"], results["y"], results["z"]] - rot = results["rot"] - self.edit_1d_JFH(t_values, r, rot) - - def get_case_key(self): - return self.case_key - - def set_case_key(self, v0_iter, cant_iter): - - self.case_key = 'vo_' + str(v0_iter) + '_cant_' + str(cant_iter) - - return - - def make_test_jfh(): - +import numpy as np +import os +import math + +from stl import mesh +import matplotlib.pyplot as plt +from mpl_toolkits import mplot3d + +from pyrpod.vehicle.LogisticsModule import LogisticsModule +from pyrpod.mission.MissionPlanner import MissionPlanner +from pyrpod.plume.RarefiedPlumeGasKinetics import SimplifiedGasKinetics + +from pyrpod.util.io.file_print import print_1d_JFH +from pyrpod.util.io.fs import ensure_dir, resolve_asset_path +from pyrpod.util.stl.stl import load_stl, transform_mesh + +from tqdm import tqdm +from queue import Queue + +from pyrpod.logging_utils import get_logger +from pyrpod.util.math.transform import rotation_matrix_from_vectors + +# New modular imports for refactor +from pyrpod.rpod.approach_maneuvers import ( + ApproachInputs, + compute_1d_approach, +) +from pyrpod.rpod.io import ensure_results_dirs, write_jfh +from pyrpod.rpod.PlumeStudyExport import PlumeStudyExport +from pyrpod.plume.PlumeStrikeCalculator import ( + compute_face_centroids, + compute_plume_strikes, + extract_plume_params, + run_parallel_plume_strikes, +) + +logger = get_logger("pyrpod.rpod.PlumeStrikeEstimationStudy") + +class PlumeStrikeEstimationStudy (MissionPlanner): + """ + Class responsible for analyzing RPOD performance of visiting vehicles. + + Caculated metrics (outputs) include propellant usage, plume impingement, + trajectory character, and performance with respect to factors of safety. + + Data Inputs inlcude (redundant? better said in user guide?) + 1. LogisticsModule (LM) object with properly defined RCS configuration + 2. Jet Firing History including LM location and orientation with repsect to the Gateway. + 3. Selected plume models for impingement analysis. + 4. Surface mesh data for target and visiting vehicle. + + Attributes + ---------- + + vv : LogisticsModule + Visiting vehicle of interest. Includes complete RCS configuration and surface mesh data. + + jfh : JetFiringHistory + Includes VV location and orientation with respect to the TV. + + plume_model : PlumeModel + Contains the relevant governing equations selected for analysis. + + Methods + ------- + study_init(self, JetFiringHistory, Target, Vehicle) + Designates assets for RPOD analysis. + + graph_init_config(self) + Creates visualization data for initiial configuration of RPOD analysis. + + graph_jfh_thruster_check(self) + Creates visualization data for initiial configuration of RPOD analysis. + + graph_clusters(self, firing, vv_orientation) + Creates visualization data for the cluster. + + graph_jfh(self) + Creates visualization data for the trajectory of the proposed RPOD analysis. + + update_window_queue(self, window_queue, cur_window, firing_time, window_size) + Takes the most recent window of time size, and adds the new firing time to the sum, and the window_queue. + If the new window is larger than the allowed window_size, then earliest firing times are removed + from the queue and subtracted from the cur_window sum, until the sum fits within the window size. + A counter for how many firing times are removed and subtracted is recorded. + + update_parameter_queue(self, param_queue, param_window_sum, get_counter) + Takes the current parameter_queue, and removes the earliest tracked parameters from the front of the queue. + This occurs "get_counter" times. Each time a parameter is popped from the queue, the sum is also updated, + as to not track the removed parameter (ie. subtract the value) + + jfh_plume_strikes(self) + Calculates number of plume strikes according to data provided for RPOD analysis. + Method does not take any parameters but assumes that study assets are correctly configured. + These assets include one JetFiringHistory, one TargetVehicle, and one VisitingVehicle. + A Simple plume model is used. It does not calculate plume physics, only strikes. which + are determined with a user defined "plume cone" geometry. Simple vector mathematics is + used to determine if an VTK surface elements is struck by the "plume cone". + + print_jfh_1d_approach(v_ida, v_o, r_o) + Method creates JFH data for axial approach using simpified physics calculations. + """ + # def __init__(self): + # print("Initialized Approach Visualizer") + def study_init(self, JetFiringHistory, Target, Vehicle): + """ + Designates assets for RPOD analysis. + + Parameters + ---------- + JetFiringHistory : JetFiringHistory + Object thruster firing history. It includes VV position, orientation, and IDs for active thrusters. + + Target : TargetVehicle + Object containing surface mesh and thruster configurations for the Visiting Vehicle. + + Vehicle : VisitingVehicle + Object containing surface mesh and surfave properties for the Target Vehicle. + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + """ + self.jfh = JetFiringHistory + self.target = Target + self.vv = Vehicle + # visualization/export helper + self.viz = PlumeStudyExport(self.environment) + + def graph_init_config(self): + """ + Creates visualization data for initiial configuration of RPOD analysis. + + NOTE: Method does not take any parameters. It assumes that self.environment.case_dir + and self.environment.config are instatiated correctly. Potential defensive programming statements? + + TODO: Needs to be re-factored to save VTK data in proper case directory. + + Returns + ------- + Method doesn't currently return anything. Simply produces data as needed. + Does the method need to return a status message? or pass similar data? + """ + + # Save first coordinate in the JFH + vv_initial_firing = self.jfh.JFH[0] + # Log initial configuration details for debugging + logger.debug("Initial VV firing position: %s", vv_initial_firing['xyz']) + + # Translate VV STL to first coordinate + self.vv.mesh.translate(vv_initial_firing['xyz']) + + # Combine target and VV STLs into one "Mesh" object. + combined = mesh.Mesh(np.concatenate( + [self.target.mesh.data, self.vv.mesh.data] + )) + + figure = plt.figure() + # axes = mplot3d.Axes3D(figure) + axes = figure.add_subplot(projection='3d') + axes.add_collection3d(mplot3d.art3d.Poly3DCollection(combined.vectors)) + # axes.quiver(X, Y, Z, U, V, W, color=(0,0,0), length=1, normalize=True) + lim = 100 + axes.set_xlim([-1 * lim, lim]) + axes.set_ylim([-1 * lim, lim]) + axes.set_zlim([-1 * lim, lim]) + axes.set_xlabel('X') + axes.set_ylabel('Y') + axes.set_zlabel('Z') + # figure.suptitle(str(i)) + plt.show() + + + def graph_jfh_thruster_check(self): + """ + Creates visualization data for initiial configuration of RPOD analysis. + + NOTE: Method does not take any parameters. It assumes that self.environment.case_dir + and self.environment.config are instatiated correctly. Potential defensive programming statements? + + TODO: Needs to be re-factored to save VTK data in proper case directory. + + Returns + ------- + Method doesn't currently return anything. Simply produces data as needed. + Does the method need to return a status message? or pass similar data? + """ + + # Link JFH numbering of thrusters to thruster names. + link = {} + i = 1 + for thruster in self.vv.thruster_data: + link[str(i)] = self.vv.thruster_data[thruster]['name'] + i = i + 1 + + # Loop through each firing in the JFH. + for firing in range(len(self.jfh.JFH)): + + # Save active thrusters for current firing. + thrusters = self.jfh.JFH[firing]['thrusters'] + + # Load and graph STL of visting vehicle. + VVmesh = load_stl('../stl/cylinder.stl') + + figure = plt.figure() + # axes = mplot3d.Axes3D(figure) + axes = figure.add_subplot(projection = '3d') + axes.add_collection3d(mplot3d.art3d.Poly3DCollection(VVmesh.vectors)) + + # Load and graph STLs of active thrusters. + for thruster in thrusters: + # Map thruster ID + thruster_id = link[str(thruster)][0] + + # Load plume STL in initial configuration. + plumeMesh = load_stl('../stl/mold_funnel.stl') + + # Tranform plume into initial configuration. + # TODO: edit mold_funnel.stl to not require these transforms. + rot_mat = np.array([ + [1, 0, 0], + [0, -1, 0], + [0, 0, -1], + ]) + plumeMesh = transform_mesh( + plumeMesh, + rotation_matrix=rot_mat, + translation_vector=[0, 0, -50], + scale_factor=0.05 + ) + + # Transform plume according to thruster and VV configuration. + plumeMesh = transform_mesh( + plumeMesh, + rotation_matrix=np.array(self.vv.thruster_data[thruster_id]['dcm']).T, + translation_vector=self.vv.thruster_data[thruster_id]['exit'][0] + ) + + logger.debug("Thruster %s DCM: %s", thruster_id, self.vv.thruster_data[thruster_id]['dcm']) + + # Add surface to graph. + surface = mplot3d.art3d.Poly3DCollection(plumeMesh.vectors) + surface.set_facecolor('orange') + axes.add_collection3d(surface) + + lim = 7 + axes.set_xlim([-1*lim - 3, lim - 3]) + axes.set_ylim([-1*lim, lim]) + axes.set_zlim([-1*lim, lim]) + axes.set_xlabel('X') + axes.set_ylabel('Y') + axes.set_zlabel('Z') + shift=0 + axes.view_init(azim=0, elev=2*shift) + + + logger.debug("Completed plotting thruster check for firing %d", firing) + + if firing < 10: + index = '00' + str(firing) + elif firing < 100: + index = '0' + str(firing) + else: + index = str(i) + # delegate figure saving to export helper + self.viz.save_figure(figure, os.path.join(self.environment.case_dir, 'img', 'frame' + str(index) + '.png')) + + def graph_clusters(self, firing, vv_orientation): + """ + Creates visualization data for the cluster. + Parameters + ---------- + firing : int + Loop iterable over the length of the number of thrusters firing in the JFH. + + vv_orientation : np.array + DCM from the JFH. + Returns + ------- + active_clusters : mesh + Cluster of the current thruster firing. + """ + active_clusters = None + clusters_list = [] + for number in range(len(self.vv.cluster_data)): + cluster_name = 'P' + str(number + 1) + # print(cluster_name) + clusters_list.append(cluster_name) + + # print('clusters_list is', clusters_list) + # Load and graph STLs of active clusters. + for cluster in clusters_list: + + # Load plume STL in initial configuration. + clusterMesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_cluster'])) + + # Transform cluster + + # First, according to DCM of current cluster in CCF + cluster_orientation = np.array( + self.vv.cluster_data[cluster]['dcm'] + ) + clusterMesh.rotate_using_matrix(cluster_orientation.transpose()) + + # Second, according to DCM of VV in JFH + clusterMesh.rotate_using_matrix(vv_orientation.transpose()) + + # Third, according to position vector of the VV in JFH + clusterMesh.translate(self.jfh.JFH[firing]['xyz']) + + # Fourth, according to position of current cluster in CCF + clusterMesh.translate(self.vv.cluster_data[cluster]['exit'][0]) + # print(self.vv.cluster_data[cluster]['exit'][0]) + + if active_clusters == None: + active_clusters = clusterMesh + else: + active_clusters = mesh.Mesh( + np.concatenate([active_clusters.data, clusterMesh.data]) + ) + return active_clusters + + def graph_jfh(self, trade_study = False): + """ + Creates visualization data for the trajectory of the proposed RPOD analysis. + + This method does NOT calculate plume strikes. + + This utilities allows engineers to visualize the trajectory in the JFH before running + the full simulation and wasting computation time. + Returns + ------- + Method doesn't currently return anything. Simply produces data as needed. + Does the method need to return a status message? or pass similar data? + """ + # Link JFH numbering of thrusters to thruster names. + link = {} + i = 1 + for thruster in self.vv.thruster_data: + link[str(i)] = self.vv.thruster_data[thruster]['name'] + i = i + 1 + + # Create results directory if it doesn't already exist. + results_dir = self.environment.case_dir + 'results' + if not os.path.isdir(results_dir): + # print("results dir doesn't exist") + os.mkdir(results_dir) + + + if not trade_study: + results_dir = results_dir + "/jfh" + if not os.path.isdir(results_dir): + #print("results dir doesn't exist") + os.mkdir(results_dir) + + if trade_study: + v_o = ['vo_0', 'vo_1', 'vo_2', 'vo_3', 'vo_4'] + cants = ['cant_0', 'cant_1', 'cant_2', 'cant_3', 'cant_4'] + for v in v_o: + for cant in cants: + results_dir_case = results_dir + "/" + v + '_' + cant + if not os.path.isdir(results_dir_case): + #print("results dir doesn't exist") + os.mkdir(results_dir_case) + + # Save STL surface of target vehicle to local variable. + target = self.target.mesh + + # Loop through each firing in the JFH. + for firing in range(len(self.jfh.JFH)): + # print('firing =', firing+1) + + # Save active thrusters for current firing. + thrusters = self.jfh.JFH[firing]['thrusters'] + # print("thrusters", thrusters) + + # Load, transform, and, graph STLs of visiting vehicle. + VVmesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_lm'])) + vv_orientation = np.array(self.jfh.JFH[firing]['dcm']) + # print(vv_orientation.transpose()) + VVmesh.rotate_using_matrix(vv_orientation.transpose()) + VVmesh.translate(self.jfh.JFH[firing]['xyz']) + + active_cones = None + + # Load and graph STLs of active clusters. + if self.vv.use_clusters == True: + active_clusters = self.graph_clusters(firing, vv_orientation) + + # Load and graph STLs of active thrusters. + for thruster in thrusters: + + + # Save thruster id using indexed thruster value. + # Could naming/code be more clear? + # print('thruster num', thruster, 'thruster id', link[str(thruster)][0]) + thruster_id = link[str(thruster)][0] + + # Load plume STL in initial configuration. + plumeMesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_thruster'])) + + # Transform plume + + # First, according to DCM of current thruster id in TCF + thruster_orientation = np.array( + self.vv.thruster_data[thruster_id]['dcm'] + ) + plumeMesh.rotate_using_matrix(thruster_orientation.transpose()) + + # Second, according to DCM of VV in JFH + plumeMesh.rotate_using_matrix(vv_orientation.transpose()) + + # Third, according to position vector of the VV in JFH + plumeMesh.translate(self.jfh.JFH[firing]['xyz']) + + # Fourth, according to position of current cluster in CCF + if self.vv.use_clusters == True: + # thruster_id[0] = "P" and thruster_id[1] = "#", adding these gives the cluster identifier + plumeMesh.translate(self.vv.cluster_data[thruster_id[0] + thruster_id[1]]['exit'][0]) + + # Fifth, according to exit vector of current thruster id in TCD + plumeMesh.translate(self.vv.thruster_data[thruster_id]['exit'][0]) + + # Takeaway: Do rotations before translating away from the rotation axes! + + + if active_cones == None: + active_cones = plumeMesh + else: + active_cones = mesh.Mesh( + np.concatenate([active_cones.data, plumeMesh.data]) + ) + + # print('DCM: ', self.vv.thruster_data[thruster_id]['dcm']) + # print('DCM: ', thruster_orientation[0], thruster_orientation[1], thruster_orientation[2]) + + if self.vv.use_clusters != True: + if not active_cones == None: + VVmesh = mesh.Mesh( + np.concatenate([VVmesh.data, active_cones.data]) + ) + if self.vv.use_clusters == True: + if not active_cones == None: + VVmesh = mesh.Mesh( + np.concatenate([VVmesh.data, active_cones.data, active_clusters.data]) + ) + + # print(self.vv.mesh) + + # print(self.environment.case_dir + self.environment.config['stl']['vv']) + + if trade_study == False: + path_to_stl = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{firing}.stl") + elif trade_study == True: + path_to_stl = os.path.join(self.environment.case_dir, "results", self.get_case_key(), "jfh", f"firing-{firing}.stl") + + # self.vv.convert_stl_to_vtk(path_to_vtk, mesh =VVmesh) + self.viz.export_firing(VVmesh, path_to_stl) + + def visualize_sweep(self, config_iter): + """ + Creates visualization data for the trajectory of the proposed RPOD analysis. + + This method is valid for SINGLE JFH firings, due to file naming conventions. + Numbering of files is based on the iteration number of the current configuration provided. + + Method used for mdao_unit_test_02.py + + This utility allows engineers to visualize a configuration sweep before running + the full simulation and wasting computational resources. + Parameters + ---------- + None + + Returns + ------- + Method doesn't currently return anything. Simply produces data as needed. + Does the method need to return a status message? or pass similar data? + """ + # Link JFH numbering of thrusters to thruster names. + link = {} + i = 1 + for thruster in self.vv.thruster_data: + link[str(i)] = self.vv.thruster_data[thruster]['name'] + i = i + 1 + # print('link is', link) + + # Create results directory if it doesn't already exist. + results_dir = self.environment.case_dir + 'results' + if not os.path.isdir(results_dir): + # print("results dir doesn't exist") + os.mkdir(results_dir) + + results_dir = results_dir + "/jfh" + if not os.path.isdir(results_dir): + # print("results dir doesn't exist") + os.mkdir(results_dir) + + # Save STL surface of target vehicle to local variable. + target = self.target.mesh + + # Loop through each firing in the JFH. + for firing in range(len(self.jfh.JFH)): + # print('firing =', firing+1) + + # Save active thrusters for current firing. + thrusters = self.jfh.JFH[firing]['thrusters'] + # print("thrusters is", thrusters) + + # Load, transform, and, graph STLs of visiting vehicle. + VVmesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_lm'])) + vv_orientation = np.array(self.jfh.JFH[firing]['dcm']) + # print(vv_orientation.transpose()) + VVmesh.rotate_using_matrix(vv_orientation.transpose()) + VVmesh.translate(self.jfh.JFH[firing]['xyz']) + + active_cones = None + + # Load and graph STLs of active clusters. + if self.vv.use_clusters == True: + active_clusters = self.graph_clusters(firing, vv_orientation) + + # Load and graph STLs of active thrusters. + for thruster in thrusters: + + thruster_id = link[str(thruster)][0] + + # Save thruster id using indexed thruster value. + # Could naming/code be more clear? + # print('thruster num', thruster, 'thruster id', link[str(thruster)][0]) + + # Load plume STL in initial configuration. + plumeMesh = mesh.Mesh.from_file(resolve_asset_path(self.environment.case_dir, 'stl', self.environment.config['vv']['stl_thruster'])) + + # Transform plume + + # First, according to DCM of current thruster id in TCF + thruster_orientation = np.array( + self.vv.thruster_data[thruster_id]['dcm'] + ) + plumeMesh.rotate_using_matrix(thruster_orientation.transpose()) + + # Second, according to DCM of VV in JFH + plumeMesh.rotate_using_matrix(vv_orientation.transpose()) + + # Third, according to position vector of the VV in JFH + plumeMesh.translate(self.jfh.JFH[firing]['xyz']) + + # Fourth, according to position of current cluster in CCF + if self.vv.use_clusters == True: + # thruster_id[0] = "P" and thruster_id[1] = "#", adding these gives the cluster identifier + plumeMesh.translate(self.vv.cluster_data[thruster_id[0] + thruster_id[1]]['exit'][0]) + + # Fifth, according to exit vector of current thruster id in TCD + plumeMesh.translate(self.vv.thruster_data[thruster_id]['exit'][0]) + + # Takeaway: Do rotations before translating away from the rotation axes! + + + if active_cones == None: + active_cones = plumeMesh + else: + active_cones = mesh.Mesh( + np.concatenate([active_cones.data, plumeMesh.data]) + ) + + # print('DCM: ', self.vv.thruster_data[thruster_id]['dcm']) + # print('DCM: ', thruster_orientation[0], thruster_orientation[1], thruster_orientation[2]) + + if self.vv.use_clusters != True: + if not active_cones == None: + VVmesh = mesh.Mesh( + np.concatenate([VVmesh.data, active_cones.data]) + ) + if self.vv.use_clusters == True: + if not active_cones == None: + VVmesh = mesh.Mesh( + np.concatenate([VVmesh.data, active_cones.data, active_clusters.data]) + ) + + # print(self.vv.mesh) + + # print(self.environment.case_dir + self.environment.config['stl']['vv']) + + if self.count > 0: + path_to_vtk = os.path.join(self.environment.case_dir, "results", "strikes", f"firing-{self.count}-{firing}") + path_to_stl = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{self.count}-{firing}.stl") + else: + path_to_vtk = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{firing}") + path_to_stl = os.path.join(self.environment.case_dir, "results", "jfh", f"firing-{firing}.stl") + # self.vv.convert_stl_to_vtk(path_to_vtk, mesh =VVmesh) + self.viz.export_firing(VVmesh, path_to_stl) + + # def update_window_queue(self, window_queue, cur_window, firing_time, window_size): + # """ + # Takes the most recent window of time size, and adds the new firing time to the sum, and the window_queue. + # If the new window is larger than the allowed window_size, then earliest firing times are removed + # from the queue and subtracted from the cur_window sum, until the sum fits within the window size. + # A counter for how many firing times are removed and subtracted is recorded. + + # Parameters + # ---------- + # window_queue : Queue + # Queue holding the tracked firing times + # cur_window : float + # sum of the tracked firing times (s) + # firing_time : float + # length of current firing (s) + # window_size : float + # max length of a window of time to track (s) + + # Returns + # ------- + # Queue + # Stores tracked firing times after update + # float + # sum of tracked firing times after update + # int + # number of firings removed from the queue + # """ + # window_queue.put(firing_time) + # cur_window += firing_time + # get_counter = 0 + # while cur_window > window_size: + # old_firing_time = window_queue.get() + # cur_window -= old_firing_time + # get_counter +=1 + # return window_queue, cur_window, get_counter + + # def update_parameter_queue(self, param_queue, param_window_sum, get_counter): + # """ + # Takes the current parameter_queue, and removes the earliest tracked parameters from the front of the queue. + # This occurs "get_counter" times. Each time a parameter is popped from the queue, the sum is also updated, + # as to not track the removed parameter (ie. subtract the value) + + # Parameters + # ---------- + # param_queue : Queue + # Queue holding the tracked parameters per firing + # param_window_sum : float + # sum of the parameters in all the tracked firings + # get_counter : int + # number of times to remove a tracked parameter from the front of the queue + + # Returns + # ------- + # Queue + # stores tracked paramters per firing after update + # float + # sum of tracked parameter after update + # """ + # for i in range(get_counter): + # old_param = param_queue.get() + # param_window_sum -= old_param + # return param_queue, param_window_sum + + # Helper functions for jfh_plume_strikes + def create_results_dir(self): + """ + Creates a results directory and sub-directories if they don't already exist. + """ + sub_dirs = ['results', 'results/strikes', 'results/jfh'] + for sub_dir in sub_dirs: + ensure_dir(os.path.join(self.environment.case_dir, sub_dir)) + + def set_strike_fields(self, target): + # Initiate array containing cummulative strikes. + cum_strikes = np.zeros(len(target.vectors)) + + # using plume physics? + if self.environment.config['pm']['kinetics'] != 'None': + + # Initiate array containing max pressures induced on each element. + max_pressures = np.zeros(len(target.vectors)) + + # Initiate array containing max shears induced on each element. + max_shears = np.zeros(len(target.vectors)) + + # Initiate array containing cummulative heatflux. + cum_heat_flux_load = np.zeros(len(target.vectors)) + + return cum_strikes, max_pressures, max_shears, cum_heat_flux_load + + return cum_strikes + + def extract_firing_data(self, firing): + # Save active thrusters for current firing. + thrusters = self.jfh.JFH[firing]['thrusters'] + # print("thrusters", thrusters) + + # Load visiting vehicle position and orientation + vv_pos = self.jfh.JFH[firing]['xyz'] + + vv_orientation = np.array(self.jfh.JFH[firing]['dcm']).transpose() + + return thrusters, vv_pos, vv_orientation + + def set_plume_strike_fields(self, target): + # reset strikes for each firing + strikes = np.zeros(len(target.vectors)) + + if self.environment.config['pm']['kinetics'] != 'None': + # reset pressures for each firing + pressures = np.zeros(len(target.vectors)) + + # reset shear pressures for each firing + shear_stresses = np.zeros(len(target.vectors)) + + # reset heat fluxes for each firing + heat_flux = np.zeros(len(target.vectors)) + heat_flux_load = np.zeros(len(target.vectors)) + return strikes, pressures, shear_stresses, heat_flux, heat_flux_load + else: + return strikes + + def set_plume_transformations(self, thruster_id, vv_orientation, vv_pos): + # Load data to calculate plume transformations + + # First, according to DCM and exit vector using current thruster id in TCD + thruster_orientation = np.array( + self.vv.thruster_data[thruster_id]['dcm'] + ).transpose() + + thruster_orientation = thruster_orientation.dot(vv_orientation) + # print('DCM: ', self.vv.thruster_data[thruster_id]['dcm']) + # print('DCM: ', thruster_orientation[0], thruster_orientation[1], thruster_orientation[2]) + plume_normal = np.array(thruster_orientation[0]) + # print("plume normal: ", plume_normal) + + # calculate thruster exit coordinate with respect to the Target Vehicle. + + # print(self.vv.thruster_data[thruster_id]) + thruster_pos = vv_pos + np.array(self.vv.thruster_data[thruster_id]['exit']) + thruster_pos = thruster_pos[0] + # print('thruster position', thruster_pos) + + return plume_normal, thruster_pos, thruster_orientation + + def set_face_centroid(self, face): + # Calculate centroid for face + + x = np.array(face[0]).mean() + y = np.array(face[1]).mean() + z = np.array(face[2]).mean() + + centroid = np.array([x, y, z]) + + return centroid + + def set_face_distance(self, thruster_pos, centroid): + # Calculate distance vector between face centroid and thruster exit. + distance = thruster_pos - centroid + # print('distance vector', distance) + norm_distance = np.sqrt(distance[0]**2 + distance[1]**2 + distance[2]**2) + + unit_distance = distance / norm_distance + # print('distance magnitude', norm_distance) + + return distance, norm_distance, unit_distance + + def _resolve_parallel_options(self, parallel, workers, n_firings): + """ + Resolves parallel execution settings for jfh_plume_strikes(). + + Precedence: explicit method arguments override the optional + [exec] config section, which defaults to serial execution. + + Config keys (both optional): + - [exec] parallel : bool — enable process-based parallelization + across firings (default false). + - [exec] workers : int — number of worker processes. Defaults to + min(os.cpu_count(), n_firings) when parallel is enabled. + + Returns + ------- + (bool, int) + (parallel_enabled, workers) — workers is capped at n_firings; + workers <= 1 resolves to serial execution. + """ + config = self.environment.config + if parallel is None: + try: + parallel = config.getboolean('exec', 'parallel', fallback=False) + except ValueError as exc: + raise ValueError( + "Invalid config value for [exec] parallel: expected a " + "boolean (true/false)." + ) from exc + if workers is None: + try: + workers = config.getint('exec', 'workers', fallback=None) + except ValueError as exc: + raise ValueError( + "Invalid config value for [exec] workers: expected a " + "positive integer." + ) from exc + if workers is not None and workers < 1: + raise ValueError( + f"workers must be a positive integer, got {workers}." + ) + + if not parallel: + return False, 1 + + if workers is None: + workers = min(os.cpu_count() or 1, n_firings) + # Never spawn more workers than there are firings to compute. + workers = min(workers, n_firings) + if workers <= 1: + return False, 1 + return True, workers + + def jfh_plume_strikes(self, parallel=None, workers=None): + """ + Calculates number of plume strikes according to data provided for RPOD analysis. + Method assumes that study assets are correctly configured. + These assets include one JetFiringHistory, one TargetVehicle, and one VisitingVehicle. + A Simple plume model is used. It does not calculate plume physics, only strikes. which + are determined with a user defined "plume cone" geometry. Simple vector mathematics is + used to determine if an VTK surface elements is struck by the "plume cone". + + Parameters + ---------- + parallel : bool, optional + Enable process-based parallelization across firings. Defaults + to None, meaning "use the optional [exec] parallel config key", + which itself defaults to false (serial, legacy behavior). + workers : int, optional + Number of worker processes when parallel execution is enabled. + Defaults to None, meaning "use the optional [exec] workers + config key", falling back to min(os.cpu_count(), n_firings). + + Notes + ----- + Only independent per-firing strike arrays are computed in worker + processes; cumulative arrays (cum_strikes, max_pressures, + max_shears, cum_heat_flux_load) are accumulated serially in + original firing order, and VTK output is written serially by the + parent process. If the parallel path fails to initialize or run, + the method logs a warning and falls back to serial execution. + + Returns + ------- + dict + firing_data keyed by firing number ('1'..'N'), each holding + per-face arrays: strikes, cum_strikes, and, when kinetics is + enabled, pressures, max_pressures, shear_stress, max_shears, + heat_flux_rate, heat_flux_load, cum_heat_flux_load. + """ + # Prepare results directories and target data + self.create_results_dir() + target = self.target.mesh + target_normals = target.get_unit_normals() + # The target is stationary for the whole run, so face centroids are + # computed once here and reused for every firing. + target_centroids = compute_face_centroids(target.vectors) + + # Initialize cumulative arrays + kinetics_on = self.environment.config['pm']['kinetics'] != 'None' + if kinetics_on: + cum_strikes, max_pressures, max_shears, cum_heat_flux_load = self.set_strike_fields(target) + else: + cum_strikes = self.set_strike_fields(target) + + firing_data = {} + + n_firings = len(self.jfh.JFH) + + # Build serializable step dicts once; shared by serial and parallel paths. + steps = [] + for firing in range(n_firings): + steps.append({ + 'thrusters': self.jfh.JFH[firing]['thrusters'], + 'xyz': np.array(self.jfh.JFH[firing]['xyz']), + 'dcm': np.array(self.jfh.JFH[firing]['dcm']), + 't': float(self.jfh.JFH[firing]['t']) + }) + + parallel_enabled, n_workers = self._resolve_parallel_options(parallel, workers, n_firings) + + # Optionally compute independent per-firing results in worker + # processes. Workers receive only plain serializable inputs (arrays, + # dicts, config scalars) — never the study/vehicle/environment objects. + per_firing_results = None + if parallel_enabled: + try: + per_firing_results = run_parallel_plume_strikes( + jfh_steps=steps, + face_centroids=target_centroids, + target_unit_normals=target_normals, + thruster_data=self.vv.thruster_data, + thruster_metrics=getattr(self.vv, 'thruster_metrics', None), + plume_params=extract_plume_params(self.environment), + workers=n_workers, + ) + except Exception as exc: + logger.warning( + "Parallel plume strike execution failed (%s: %s); " + "falling back to serial execution.", + type(exc).__name__, exc, + ) + per_firing_results = None + + # Loop through each firing in the JFH and delegate to impingement module + for firing in range(n_firings): + step = steps[firing] + + if per_firing_results is not None: + result = per_firing_results[firing] + else: + result = compute_plume_strikes( + target_mesh=target, + target_unit_normals=target_normals, + vv=self.vv, + jfh_step=step, + environment=self.environment, + face_centroids=target_centroids, + ) + + strikes = result["strikes"] + cum_strikes = cum_strikes + strikes + + cellData = { + "strikes": strikes, + "cum_strikes": cum_strikes.copy(), + } + + if kinetics_on: + pressures = result.get("pressures") + shear_stresses = result.get("shear_stress") + heat_flux_rate = result.get("heat_flux_rate") + heat_flux_load = result.get("heat_flux_load") + + max_pressures = np.maximum(max_pressures, pressures) + max_shears = np.maximum(max_shears, shear_stresses) + cum_heat_flux_load = cum_heat_flux_load + heat_flux_load + + cellData.update({ + "pressures": pressures, + "max_pressures": max_pressures, + "shear_stress": shear_stresses, + "max_shears": max_shears, + "heat_flux_rate": heat_flux_rate, + "heat_flux_load": heat_flux_load, + "cum_heat_flux_load": cum_heat_flux_load, + }) + + firing_data[str(firing+1)] = cellData + + # if checking constraints: + # save all new pressure and heat flux values into each cell's respective queue + # add pressure and heat_flux into each cell's window sum + # update the window queues based on their max sizes, and the firing times + # update the parameter queues based on the updates made on the window queues + # if self.environment.config['pm']['kinetics'] != 'None' and checking_constraints: + # for i in range(len(pressures)): + # pressure_queues[i].put(float(pressures[i])) + # pressure_window_sums[i] += pressures[i] + + # heat_flux_queues[i].put(float(heat_flux_load[i])) + # heat_flux_window_sums[i] += heat_flux_load[i] + + # pressure_window_queue, pressure_cur_window, pressure_get_counter = self.update_window_queue( + # pressure_window_queue, pressure_cur_window, firing_time, pressure_window_size) + + # heat_flux_window_queue, heat_flux_cur_window, heat_flux_get_counter = self.update_window_queue( + # heat_flux_window_queue, heat_flux_cur_window, firing_time, heat_flux_window_size) + + # for queue_index in range(len(pressure_queues)): + # if not pressure_queues[queue_index].empty(): + # pressure_queues[queue_index], pressure_window_sums[queue_index] = self.update_parameter_queue(pressure_queues[queue_index], pressure_window_sums[queue_index], pressure_get_counter) + + # if not heat_flux_queues[queue_index].empty() and heat_flux_window_sums[queue_index] != 0: + # heat_flux_queues[queue_index], heat_flux_window_sums[queue_index] = self.update_parameter_queue(heat_flux_queues[queue_index], heat_flux_window_sums[queue_index], heat_flux_get_counter) + + # #check if instantaneous constraints are broken + # if pressures[queue_index] > pressure_constraint and not failed_constraints: + # constraint_file.write(f"Pressure constraint failed at cell #{i}.\n") + # constraint_file.write(f"Pressure reached {pressures[queue_index]}.\n\n") + # failed_constraints = 1 + # if shear_stresses[queue_index] > shear_constraint and not failed_constraints: + # constraint_file.write(f"Shear constraint failed at cell #{i}.\n") + # constraint_file.write(f"Shear reached {shear_stresses[queue_index]}.\n\n") + # failed_constraints = 1 + # if heat_flux[queue_index] > heat_flux_constraint and not failed_constraints: + # constraint_file.write(f"Heat flux constraint failed at cell #{i}.\n") + # constraint_file.write(f"Heat flux reached {heat_flux[queue_index]}.\n\n") + # failed_constraints = 1 + + # # check if window constraints are broken, and report + # if pressure_window_sums[queue_index] > pressure_window_constraint and not failed_constraints: + # constraint_file.write(f"Pressure window constraint failed at cell #{queue_index}.\n") + # constraint_file.write(f"Pressure winodw reached {pressure_window_sums[queue_index]}.\n\n") + # failed_constraints = 1 + # if heat_flux_window_sums[queue_index] > heat_flux_window_constraint and not failed_constraints: + # constraint_file.write(f"Heat flux window constraint failed at cell #{queue_index}.\n") + # constraint_file.write(f"Heat flux load reached {heat_flux_window_sums[queue_index]}.\n\n") + # failed_constraints = 1 + + + path_to_vtk = self.environment.case_dir + "results/strikes/firing-" + str(firing) + + # print(cellData) + # input() + self.target.convert_stl_to_vtk_strikes(path_to_vtk, cellData.copy(), target) + + # if self.environment.config['pm']['kinetics'] != 'None' and checking_constraints: + # if not failed_constraints: + # constraint_file.write(f"All impingement constraints met.") + # # constraint_file.close() + + return firing_data + + def calc_time_multiplier(self, v_ida, v_o, r_o): + # Determine thruster configuration characterstics. + # The JFH only contains firings done by the neg_x group + m_dot_sum = self.calc_m_dot_sum('neg_x') + # print('m_dot_sum is', m_dot_sum) + MIB = self.vv.thruster_metrics[self.vv.thruster_data[self.vv.rcs_groups['neg_x'][0]]['type'][0]]['MIB'] + # print('MIB is', MIB) + F_thruster = self.vv.thruster_metrics[self.vv.thruster_data[self.vv.rcs_groups['neg_x'][0]]['type'][0]]['F'] + F = F_thruster * np.cos(self.vv.decel_cant) + n_thrusters = len(self.vv.rcs_groups['neg_x']) + F = F * n_thrusters + + # Defining a multiplier reduce time steps and make running faster + dt = (MIB / F_thruster) + # print('dt is', dt) + dm_firing = m_dot_sum * dt + # print('dm_firing is', dm_firing) + docking_mass = self.vv.mass + # print('docking mass is', docking_mass) + + # Calculate required change in velocity. + dv_req = v_o - v_ida + v_e = self.calc_v_e('neg_x') + + # Calculate propellant used for docking and changes in mass. + forward_propagation = False + delta_mass_jfh = self.calc_delta_mass_v_e(dv_req, v_e, forward_propagation) + # print('delta_mass_docking is',delta_mass_jfh) + + pre_approach_mass = self.vv.mass + # print('pre-approach mass is', pre_approach_mass) + + # Instantiate data structure to hold JFH data + physics data. + # Initializing position + x = [r_o] + y = [0] + z = [0] + + # Initializing empty tracking lists + dx = [0] + t = [0] + dv = [0] + + # Initializing inertial state + dxdt = [v_o] + + # Initializing initial mass + mass = [pre_approach_mass] + + # Initializing list to later sum propellant expenditure + dm_total = [dm_firing] + + # Firing number + n = [1] + + # Create dummy rotation matrices. + x1 = [1, 0, 0] + y1 = [1, 0, 0] + + rot = [np.array(rotation_matrix_from_vectors(x1, y1))] + + # Calculate JFH and 1D physics data for required firings. + while (dv_req > 0): + # print('dv_req', round(dv_req, 4), 'n firings', n[i]) + + # Grab last value in the JFH arrays (initial conditions for current time step) + # print('x, dx, dt, t, dxdt, mass, dm_total') + # print(x[-1], dx[-1], dt_vals[-1], t[-1], dxdt[-1], mass[-1], dm_total[-1]) + + # Update VV mass per firing + mass_o = mass[-1] + mass.append(mass_o - dm_firing) + mass_f = mass[-1] + # print('mass_f is', mass_f) + + # Calculate velocity change per firing. + dv_firing = self.calc_delta_v(dt, v_e, m_dot_sum, mass_o) + dv.append(dv_firing) + # print('dv is', dv_firing) + # print(round(dxdt[-1] - dv_firing, 2)) + # input() + dxdt.append(dxdt[-1] - dv_firing) + + # Calculate distance traveled per firing + # print(dxdt[-1], dxdt[-2]) # last and second to last element. + v_avg = 0.5 * (dxdt[-1] + dxdt[-2]) + dx.append(v_avg * dt) + x.append(x[-1] - v_avg*dt) + y.append(0) + z.append(0) + + # Calculate left over v_req (TERMINATES LOOP) + dv_req -= dv_firing + + # Calculate mass expended up to this point. + dm_total.append(dm_total[-1] + dm_firing) + + # Calculate current firing. + n.append(n[-1]+1) + + # Add time data. + t.append(t[-1] + dt) + + rot.append(np.array(rotation_matrix_from_vectors(x1, y1))) + + # After simulating, estimate a coarse time multiplier based on number of firings + n_firings = len(t) + time_multiplier = 10 / n_firings if n_firings > 0 else 1 + # print('n firings:', len(t), 'time multiplier:', time_multiplier) + return (1 / time_multiplier) + + # one_d_results = { + # 'n_firings': n, + # 'x': x, + # 'dx': dx, + # 't': t, + # 'dv': dv, + # 'v': dxdt, + # 'mass': mass, + # 'delta_mass': dm_total + # } + + # print(one_d_results) + + def print_jfh_1d_approach_n_fire(self, v_ida, v_o, r_o, n_firings, trade_study = False): + # Delegate to approach_maneuvers.compute_1d_approach and rpod.io.write_jfh + tm = self.calc_time_multiplier(v_ida, v_o, r_o) + inputs = ApproachInputs(v_ida=float(v_ida), v_o=float(v_o), r_o=float(r_o), group='neg_x') + # Adapter for grouping methods if not explicitly available as a module + class _GroupingAdapter: + def __init__(self, outer): + self._outer = outer + def calc_m_dot_sum(self, group): + return self._outer.calc_m_dot_sum(group) + def calc_v_e(self, group): + return self._outer.calc_v_e(group) + + results = compute_1d_approach( + inputs=inputs, + vv=self.vv, + fuel_mgr=self, + grouping=_GroupingAdapter(self), + cant_rad=self.vv.decel_cant, + dt_strategy={"multiplier": tm}, + ) + + r = [results["x"], results["y"], results["z"]] + t_values = results["t"] + rot = results["rot"] + + # Build output path as before + if not trade_study: + jfh_path = self.environment.case_dir + 'jfh/' + self.environment.config['jfh']['jfh'] + else: + jfh_path = self.environment.case_dir + 'jfh/' + self.get_case_key() + '.A' + + os.makedirs(os.path.dirname(jfh_path), exist_ok=True) + write_jfh(t_values, r, rot, jfh_path, mode="1d") + + + def print_jfh_1d_approach(self, v_ida, v_o, r_o, trade_study = False): + """ + Method creates JFH data for axial approach using simpified physics calculations. + + This approach models one continuous firing. + + Kinematics and mass changes are discretized according to the thruster's minimum firing time. + + Parameters + ---------- + v_ida : float + VisitingVehicle docking velocity (determined by international docking adapter) + + v_o : float + VisitingVehicle incoming axial velocity. + + r_o : float + Initial distance to docking port. + + Returns + ------- + Method doesn't currently return anything. Simply prints data to a files as needed. + Does the method need to return a status message? or pass similar data? + + """ + # Delegate to approach_maneuvers with a fixed multiplier similar to legacy + inputs = ApproachInputs(v_ida=float(v_ida), v_o=float(v_o), r_o=float(r_o), group='neg_x') + class _GroupingAdapter: + def __init__(self, outer): + self._outer = outer + def calc_m_dot_sum(self, group): + return self._outer.calc_m_dot_sum(group) + def calc_v_e(self, group): + return self._outer.calc_v_e(group) + + results = compute_1d_approach( + inputs=inputs, + vv=self.vv, + fuel_mgr=self, + grouping=_GroupingAdapter(self), + cant_rad=self.vv.decel_cant, + dt_strategy={"multiplier": 60.0}, + ) + + r = [results["x"], results["y"], results["z"]] + t_values = results["t"] + rot = results["rot"] + + if not trade_study: + jfh_path = self.environment.case_dir + 'jfh/' + self.environment.config['jfh']['jfh'] + else: + jfh_path = self.environment.case_dir + 'jfh/' + self.get_case_key() + '.A' + + os.makedirs(os.path.dirname(jfh_path), exist_ok=True) + write_jfh(t_values, r, rot, jfh_path, mode="1d") + return + + def edit_1d_JFH(self, t_values, r, rot): + """ + Helper function to RPOD.calc_jfh_1d_approach() that is responsible for + modifying the JFH attribute in memory with the values calculated. + + Parameters + ---------- + t_values : np.array + Array containing time step data for each firing in the JFH. + + r : np.array + Array containing positional data for each firing in the JFH. + + rot : np.array + Array containing rotational data for each firing in the JFH. + + Returns + ------- + Method doesn't currently return anything. Simply prints data to a files as needed. + Does the method need to return a status message? or pass similar data? + + """ + # Printing out the empty JFH and checking its size + # print('self.jfh.JFH is', self.jfh.JFH) + # print('len(self.jfh.JFH) is', len(self.jfh.JFH)) + + # Scrap + # print('self.jfh.JFH[0] is', self.jfh.JFH[0]) + # print('self.jfh.JFH[1] is', self.jfh.JFH[1]) + # self.jfh.JFH.append({'nt': '1', 'dt': '0.000', 't': '1', 'dcm': [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], 'xyz': [-10.0, 0.0, 0.0], 'uf': 1.0, 'thrusters': [1, 2, 5, 6, 9, 10, 13, 14]}) + # print('self.jfh.JFH[0] is', self.jfh.JFH[0]) + + # Checking the time step + # print('t_values is', t_values) + # print('len(t_values) is', len(t_values)) + + # Changing the number of firings to be evaluated + self.jfh.nt = len(t_values) + # print('self.jfh.nt is', self.jfh.nt) + # print('self.jfh.nt after is', self.jfh.nt) + # print("self.jfh.JFH[0]['nt'] is", self.jfh.JFH[0]['nt']) + + # Confirming the time step is correct + # print('str(t_values[1]) is', str(t_values[1])) + + # Verifying the format of the rot array + # print('rot[0] is', rot[0]) + # print('[list(rot[0][0]), list(rot[0][1]), list(rot[0][2])] is', [list(rot[0][0]), list(rot[0][1]), list(rot[0][2])]) + # print('[list(rot[1][0]), list(rot[1][1]), list(rot[1][2])] is', [list(rot[1][0]), list(rot[1][1]), list(rot[1][2])]) + + # Verifying the format of the r array + # print('r is', r) + # print('[r[0][0], r[1][0], r[2][0]] is', [r[0][0], r[1][0], r[2][0]]) + # print('[r[0][1], r[1][1], r[2][1]] is', [r[0][1], r[1][1], r[2][1]]) + + for i in range(len(t_values)): + # NOTE: 'thrusters' is currently hardcoded + # NOTE: all the 'xyz' values are still negative + # Start from the required distance to slow down and approach zero + # print('-r[0][-1] + r[0][i] is', -r[0][-1] + r[0][i]) + + + # NOTE: The thrusters are currently hardcoded, but should be changed to the thrusters indicated in the neg_x group in the tgf + self.jfh.JFH.append({'nt': str(i + 1), 'dt': str(t_values[i]), 't': str(t_values[1]), 'dcm': [list(rot[i][0]), list(rot[i][1]), list(rot[i][2])], 'xyz': [-r[0][-1] + r[0][i] - 0.5, -r[1][i], -r[2][i]], 'uf': 1.0, 'thrusters': [1, 2, 5, 6, 9, 10, 13, 14]}) + + # Printing out the populated JFH and checking its size + # print('self.jfh.JFH is', self.jfh.JFH) + # print('len(self.jfh.JFH) is', len(self.jfh.JFH)) + + def calc_jfh_1d_approach(self, v_ida, v_o, cant): + """ + The x-position represents the distance required to reach a velocity of zero. + + Method calculates JFH data for 1D approach using simpified physics calculations. + + This approach models one continuous firing. + + Kinematics and mass changes are discretized according to the thruster's minimum firing time. + + Parameters + ---------- + v_ida : float + VisitingVehicle docking velocity (determined by international docking adapter) + + v_o : float + VisitingVehicle incoming axial velocity. + + cant : float + Angling of all deceleration thrusters in degrees that + + Returns + ------- + Method doesn't currently return anything. Simply prints data to a files as needed. + Does the method need to return a status message? or pass similar data? + + """ + # Delegate to compute-first API, then update in-memory JFH via existing helper + MissionPlanner.cant = np.radians(cant) + inputs = ApproachInputs(v_ida=float(v_ida), v_o=float(v_o), r_o=0.0, group='neg_x') + + class _GroupingAdapter: + def __init__(self, outer): + self._outer = outer + def calc_m_dot_sum(self, group): + return self._outer.calc_m_dot_sum(group) + def calc_v_e(self, group): + return self._outer.calc_v_e(group) + + results = compute_1d_approach( + inputs=inputs, + vv=self.vv, + fuel_mgr=self, + grouping=_GroupingAdapter(self), + cant_rad=MissionPlanner.cant, + dt_strategy={"multiplier": 100.0}, + ) + + t_values = results["t"] + r = [results["x"], results["y"], results["z"]] + rot = results["rot"] + self.edit_1d_JFH(t_values, r, rot) + + def get_case_key(self): + return self.case_key + + def set_case_key(self, v0_iter, cant_iter): + + self.case_key = 'vo_' + str(v0_iter) + '_cant_' + str(cant_iter) + + return + + def make_test_jfh(): + return \ No newline at end of file diff --git a/pyrpod/rpod/PlumeStudyExport.py b/pyrpod/rpod/PlumeStudyExport.py index 9701892..65cc10e 100644 --- a/pyrpod/rpod/PlumeStudyExport.py +++ b/pyrpod/rpod/PlumeStudyExport.py @@ -1,45 +1,45 @@ -"""Small export/visualization helper for PlumeStrikeEstimationStudy. - -This isolates file-writing and optional figure saving so the planner -can delegate those responsibilities and remain focused on simulation -logic. -""" -import os -from typing import Optional - -import matplotlib.pyplot as plt - - -class PlumeStudyExport: - """Helper service for saving meshes and figures used by the study. - - The actual mesh object is expected to implement a ``save(path)`` method - (matching the existing usage in the codebase). - """ - - def __init__(self, environment): - self.environment = environment - - def _ensure_parent(self, path: str) -> None: - parent = os.path.dirname(path) - if parent: - os.makedirs(parent, exist_ok=True) - - def export_firing(self, vv_mesh, path_to_stl: str) -> None: - """Save the provided mesh to disk, ensuring directory exists. - - Keeps behavior identical to previous calls to ``mesh.save(path)`` - but centralizes directory creation and any future enhancements. - """ - self._ensure_parent(path_to_stl) - vv_mesh.save(path_to_stl) - - def save_figure(self, figure: plt.Figure, image_path: str, close: bool = True) -> None: - """Save a Matplotlib figure to disk and optionally close it. - - Ensures the output directory exists. - """ - self._ensure_parent(image_path) - figure.savefig(image_path) - if close: - plt.close(figure) +"""Small export/visualization helper for PlumeStrikeEstimationStudy. + +This isolates file-writing and optional figure saving so the planner +can delegate those responsibilities and remain focused on simulation +logic. +""" +import os +from typing import Optional + +import matplotlib.pyplot as plt + + +class PlumeStudyExport: + """Helper service for saving meshes and figures used by the study. + + The actual mesh object is expected to implement a ``save(path)`` method + (matching the existing usage in the codebase). + """ + + def __init__(self, environment): + self.environment = environment + + def _ensure_parent(self, path: str) -> None: + parent = os.path.dirname(path) + if parent: + os.makedirs(parent, exist_ok=True) + + def export_firing(self, vv_mesh, path_to_stl: str) -> None: + """Save the provided mesh to disk, ensuring directory exists. + + Keeps behavior identical to previous calls to ``mesh.save(path)`` + but centralizes directory creation and any future enhancements. + """ + self._ensure_parent(path_to_stl) + vv_mesh.save(path_to_stl) + + def save_figure(self, figure: plt.Figure, image_path: str, close: bool = True) -> None: + """Save a Matplotlib figure to disk and optionally close it. + + Ensures the output directory exists. + """ + self._ensure_parent(image_path) + figure.savefig(image_path) + if close: + plt.close(figure) diff --git a/pyrpod/rpod/approach_maneuvers.py b/pyrpod/rpod/approach_maneuvers.py index 73e22c6..42acaf9 100644 --- a/pyrpod/rpod/approach_maneuvers.py +++ b/pyrpod/rpod/approach_maneuvers.py @@ -1,133 +1,133 @@ -""" -Approach maneuvers: 1D axial burn profiles, mass/prop evolution, and JFH generation helpers. - -Design goals: -- Compute-first API: return arrays; no file I/O -- Reuse FuelManager (delta-v, delta-mass) and ThrusterGrouping (m_dot, v_e) -- Allow pluggable time-step strategies (fixed, heuristic, min-impulse-bit-based) -""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Dict, List, Tuple -import numpy as np - - -@dataclass -class ApproachInputs: - v_ida: float # docking velocity target - v_o: float # initial axial velocity (> v_ida) - r_o: float # initial range along docking axis (m) - group: str = "neg_x" # thruster group for decel - - -def choose_dt_from_mib(vv: Any, group: str, time_multiplier: float = 1.0) -> float: - """Default time step based on min impulse bit and a multiplier. - - dt ~ MIB / F_thruster scaled by time_multiplier, using the first thruster in the group. - """ - thr_name = vv.rcs_groups[group][0] - t_type = vv.thruster_data[thr_name]['type'][0] - MIB = vv.thruster_metrics[t_type]['MIB'] - F_thruster = vv.thruster_metrics[t_type]['F'] - return (MIB / F_thruster) * time_multiplier - - -def heuristic_time_multiplier(vv: Any, fuel_mgr: Any, group: str, v_ida: float, v_o: float, r_o: float) -> float: - """Run a coarse sim to estimate a time multiplier. Mirrors RPOD.calc_time_multiplier behavior.""" - # This will be implemented by porting logic from RPOD.calc_time_multiplier during refactor - raise NotImplementedError("heuristic_time_multiplier pending port from RPOD.calc_time_multiplier") - - -def compute_1d_approach( - inputs: ApproachInputs, - vv: Any, - fuel_mgr: Any, - grouping: Any, - cant_rad: float, - dt_strategy: Dict[str, Any] | None = None, -) -> Dict[str, np.ndarray]: - """Discrete 1D deceleration under constant-thrust firings. - - Returns dict with arrays: t, x, y, z, v, dv, mass, dm_total, rot - No file I/O; callers can serialize via rpod.io.write_jfh. - """ - v_ida, v_o, r_o = inputs.v_ida, inputs.v_o, inputs.r_o - group = inputs.group - - # Pre-compute thrust/mass-flow characteristics - m_dot_sum = grouping.calc_m_dot_sum(group) - v_e = grouping.calc_v_e(group) - - # Effective thrust with cant and number of thrusters - n_thrusters = len(vv.rcs_groups[group]) - t_name0 = vv.rcs_groups[group][0] - t_type0 = vv.thruster_data[t_name0]['type'][0] - F_thruster = vv.thruster_metrics[t_type0]['F'] - F_eff = np.cos(cant_rad) * F_thruster * n_thrusters - - # Choose dt - if dt_strategy and dt_strategy.get("type") == "heuristic": - tm = dt_strategy.get("multiplier") - if tm is None: - raise NotImplementedError("heuristic time multiplier not yet wired; pass explicit multiplier") - dt = choose_dt_from_mib(vv, group, time_multiplier=tm) - else: - tm = (dt_strategy or {}).get("multiplier", 1.0) - dt = choose_dt_from_mib(vv, group, time_multiplier=tm) - - dm_firing = m_dot_sum * dt - - dv_req = v_o - v_ida - - # Initialize arrays - x = [r_o] - y = [0.0] - z = [0.0] - dx = [0.0] - t = [0.0] - dv = [0.0] - v = [v_o] - mass = [vv.mass] - dm_total = [dm_firing] - rot = [] - - # Identity rotation placeholder (align x-axis) - rot_mat = np.eye(3) - rot.append(rot_mat) - - while dv_req > 0: - m_o = mass[-1] - mass.append(m_o - dm_firing) - - dv_f = fuel_mgr.calc_delta_v(dt, v_e, m_dot_sum, m_o) - dv.append(dv_f) - v.append(v[-1] - dv_f) - - v_avg = 0.5 * (v[-1] + v[-2]) - dx.append(v_avg * dt) - x.append(x[-1] - v_avg * dt) - y.append(0.0) - z.append(0.0) - - dv_req -= dv_f - dm_total.append(dm_total[-1] + dm_firing) - t.append(t[-1] + dt) - rot.append(rot_mat) - - if len(t) > 100000: - # guard against runaway loops - break - - return { - "t": np.array(t), - "x": np.array(x), - "y": np.array(y), - "z": np.array(z), - "dx": np.array(dx), - "v": np.array(v), - "dv": np.array(dv), - "mass": np.array(mass), - "dm_total": np.array(dm_total), - "rot": np.array(rot), - } +""" +Approach maneuvers: 1D axial burn profiles, mass/prop evolution, and JFH generation helpers. + +Design goals: +- Compute-first API: return arrays; no file I/O +- Reuse FuelManager (delta-v, delta-mass) and ThrusterGrouping (m_dot, v_e) +- Allow pluggable time-step strategies (fixed, heuristic, min-impulse-bit-based) +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Tuple +import numpy as np + + +@dataclass +class ApproachInputs: + v_ida: float # docking velocity target + v_o: float # initial axial velocity (> v_ida) + r_o: float # initial range along docking axis (m) + group: str = "neg_x" # thruster group for decel + + +def choose_dt_from_mib(vv: Any, group: str, time_multiplier: float = 1.0) -> float: + """Default time step based on min impulse bit and a multiplier. + + dt ~ MIB / F_thruster scaled by time_multiplier, using the first thruster in the group. + """ + thr_name = vv.rcs_groups[group][0] + t_type = vv.thruster_data[thr_name]['type'][0] + MIB = vv.thruster_metrics[t_type]['MIB'] + F_thruster = vv.thruster_metrics[t_type]['F'] + return (MIB / F_thruster) * time_multiplier + + +def heuristic_time_multiplier(vv: Any, fuel_mgr: Any, group: str, v_ida: float, v_o: float, r_o: float) -> float: + """Run a coarse sim to estimate a time multiplier. Mirrors RPOD.calc_time_multiplier behavior.""" + # This will be implemented by porting logic from RPOD.calc_time_multiplier during refactor + raise NotImplementedError("heuristic_time_multiplier pending port from RPOD.calc_time_multiplier") + + +def compute_1d_approach( + inputs: ApproachInputs, + vv: Any, + fuel_mgr: Any, + grouping: Any, + cant_rad: float, + dt_strategy: Dict[str, Any] | None = None, +) -> Dict[str, np.ndarray]: + """Discrete 1D deceleration under constant-thrust firings. + + Returns dict with arrays: t, x, y, z, v, dv, mass, dm_total, rot + No file I/O; callers can serialize via rpod.io.write_jfh. + """ + v_ida, v_o, r_o = inputs.v_ida, inputs.v_o, inputs.r_o + group = inputs.group + + # Pre-compute thrust/mass-flow characteristics + m_dot_sum = grouping.calc_m_dot_sum(group) + v_e = grouping.calc_v_e(group) + + # Effective thrust with cant and number of thrusters + n_thrusters = len(vv.rcs_groups[group]) + t_name0 = vv.rcs_groups[group][0] + t_type0 = vv.thruster_data[t_name0]['type'][0] + F_thruster = vv.thruster_metrics[t_type0]['F'] + F_eff = np.cos(cant_rad) * F_thruster * n_thrusters + + # Choose dt + if dt_strategy and dt_strategy.get("type") == "heuristic": + tm = dt_strategy.get("multiplier") + if tm is None: + raise NotImplementedError("heuristic time multiplier not yet wired; pass explicit multiplier") + dt = choose_dt_from_mib(vv, group, time_multiplier=tm) + else: + tm = (dt_strategy or {}).get("multiplier", 1.0) + dt = choose_dt_from_mib(vv, group, time_multiplier=tm) + + dm_firing = m_dot_sum * dt + + dv_req = v_o - v_ida + + # Initialize arrays + x = [r_o] + y = [0.0] + z = [0.0] + dx = [0.0] + t = [0.0] + dv = [0.0] + v = [v_o] + mass = [vv.mass] + dm_total = [dm_firing] + rot = [] + + # Identity rotation placeholder (align x-axis) + rot_mat = np.eye(3) + rot.append(rot_mat) + + while dv_req > 0: + m_o = mass[-1] + mass.append(m_o - dm_firing) + + dv_f = fuel_mgr.calc_delta_v(dt, v_e, m_dot_sum, m_o) + dv.append(dv_f) + v.append(v[-1] - dv_f) + + v_avg = 0.5 * (v[-1] + v[-2]) + dx.append(v_avg * dt) + x.append(x[-1] - v_avg * dt) + y.append(0.0) + z.append(0.0) + + dv_req -= dv_f + dm_total.append(dm_total[-1] + dm_firing) + t.append(t[-1] + dt) + rot.append(rot_mat) + + if len(t) > 100000: + # guard against runaway loops + break + + return { + "t": np.array(t), + "x": np.array(x), + "y": np.array(y), + "z": np.array(z), + "dx": np.array(dx), + "v": np.array(v), + "dv": np.array(dv), + "mass": np.array(mass), + "dm_total": np.array(dm_total), + "rot": np.array(rot), + } diff --git a/pyrpod/rpod/geometry.py b/pyrpod/rpod/geometry.py index fec5f34..952bcc7 100644 --- a/pyrpod/rpod/geometry.py +++ b/pyrpod/rpod/geometry.py @@ -1,66 +1,66 @@ -""" -Geometry utilities for RPOD visualization and mesh composition. - -Responsibilities: -- Build visiting vehicle (VV) mesh for a given firing pose -- Compose cluster and thruster plume meshes -- Provide simple transform pipelines (rotate/translate order) - -Note: This module should not perform file I/O; writing belongs in rpod.io. -""" -from __future__ import annotations - -from typing import Any, Iterable, Sequence - - -def build_vv_mesh(vv: Any, vv_orientation: Any, vv_pos: Any, environment: Any) -> Any: - """Return a transformed VV mesh for a given pose. - - Inputs - - vv: visiting vehicle object with STL/mesh handle - - vv_orientation: 3x3 DCM (array-like) - - vv_pos: position vector [x,y,z] - - environment: MissionEnvironment with case_dir/config - - Output - - mesh-like object (e.g., numpy-stl Mesh) already transformed - - Implementation is wired during refactor from RPOD.graph_jfh(). - """ - # Placeholder: actual implementation will use numpy-stl and vv/environment config - raise NotImplementedError("build_vv_mesh is pending refactor from RPOD.graph_jfh") - - -def build_cluster_mesh(vv: Any, vv_orientation: Any, vv_pos: Any, environment: Any) -> Any: - """Return combined mesh for all clusters at the current VV pose. - - Implementation will mirror RPOD.graph_clusters behavior. - """ - raise NotImplementedError("build_cluster_mesh is pending refactor from RPOD.graph_clusters") - - -def compose_thruster_plumes( - vv: Any, - active_thruster_ids: Iterable[str], - vv_orientation: Any, - vv_pos: Any, - environment: Any, -) -> Any: - """Return combined plume mesh for the active thrusters. - - Inputs - - active_thruster_ids: identifiers like 'P1#' or per current schema - - Output - - Combined mesh-like object for all active plume cones - """ - raise NotImplementedError("compose_thruster_plumes is pending refactor from RPOD.graph_jfh") - - -def transform_pipeline(mesh_obj: Any, rotations: Sequence[Any], translations: Sequence[Any]) -> Any: - """Apply rotate-then-translate operations in order and return the mesh. - - This is a small utility to centralize transform ordering to avoid mistakes - (rotate about origin before translating away from axes). - """ - raise NotImplementedError("transform_pipeline to be implemented during refactor") +""" +Geometry utilities for RPOD visualization and mesh composition. + +Responsibilities: +- Build visiting vehicle (VV) mesh for a given firing pose +- Compose cluster and thruster plume meshes +- Provide simple transform pipelines (rotate/translate order) + +Note: This module should not perform file I/O; writing belongs in rpod.io. +""" +from __future__ import annotations + +from typing import Any, Iterable, Sequence + + +def build_vv_mesh(vv: Any, vv_orientation: Any, vv_pos: Any, environment: Any) -> Any: + """Return a transformed VV mesh for a given pose. + + Inputs + - vv: visiting vehicle object with STL/mesh handle + - vv_orientation: 3x3 DCM (array-like) + - vv_pos: position vector [x,y,z] + - environment: MissionEnvironment with case_dir/config + + Output + - mesh-like object (e.g., numpy-stl Mesh) already transformed + + Implementation is wired during refactor from RPOD.graph_jfh(). + """ + # Placeholder: actual implementation will use numpy-stl and vv/environment config + raise NotImplementedError("build_vv_mesh is pending refactor from RPOD.graph_jfh") + + +def build_cluster_mesh(vv: Any, vv_orientation: Any, vv_pos: Any, environment: Any) -> Any: + """Return combined mesh for all clusters at the current VV pose. + + Implementation will mirror RPOD.graph_clusters behavior. + """ + raise NotImplementedError("build_cluster_mesh is pending refactor from RPOD.graph_clusters") + + +def compose_thruster_plumes( + vv: Any, + active_thruster_ids: Iterable[str], + vv_orientation: Any, + vv_pos: Any, + environment: Any, +) -> Any: + """Return combined plume mesh for the active thrusters. + + Inputs + - active_thruster_ids: identifiers like 'P1#' or per current schema + + Output + - Combined mesh-like object for all active plume cones + """ + raise NotImplementedError("compose_thruster_plumes is pending refactor from RPOD.graph_jfh") + + +def transform_pipeline(mesh_obj: Any, rotations: Sequence[Any], translations: Sequence[Any]) -> Any: + """Apply rotate-then-translate operations in order and return the mesh. + + This is a small utility to centralize transform ordering to avoid mistakes + (rotate about origin before translating away from axes). + """ + raise NotImplementedError("transform_pipeline to be implemented during refactor") diff --git a/pyrpod/rpod/header.py b/pyrpod/rpod/header.py index 3923546..59e5424 100644 --- a/pyrpod/rpod/header.py +++ b/pyrpod/rpod/header.py @@ -1,14 +1,14 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-06-23 - -# ======================== -# PyRPOD: test/test_header.py -# ======================== -# Pretty janky, but importing this header file into your working directory -# is a simple way to allow the source code to be run outisde of it's own directory. - -import sys - +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-06-23 + +# ======================== +# PyRPOD: test/test_header.py +# ======================== +# Pretty janky, but importing this header file into your working directory +# is a simple way to allow the source code to be run outisde of it's own directory. + +import sys + sys.path.insert(0, '../') \ No newline at end of file diff --git a/pyrpod/rpod/io.py b/pyrpod/rpod/io.py index c8e5c6f..7ecb456 100644 --- a/pyrpod/rpod/io.py +++ b/pyrpod/rpod/io.py @@ -1,39 +1,39 @@ -""" -IO helpers for RPOD: VTK/STL writers and results directory setup. - -Responsibilities: -- Create case results subdirectories -- Save meshes to STL/VTK -- Write JFH files (wraps util.io.file_print) - -Keep logic thin and parameterized; do not compute physics here. -""" -from __future__ import annotations - -import os -from typing import Any, Sequence - -from pyrpod.util.io.file_print import print_JFH, print_1d_JFH -from pyrpod.util.io.fs import ensure_dir - - -def ensure_results_dirs(case_dir: str, subdirs: Sequence[str] = ("results", "results/strikes", "results/jfh")) -> None: - for sub in subdirs: - ensure_dir(os.path.join(case_dir, sub)) - - -def save_mesh_to_stl(mesh_obj: Any, path: str) -> None: - """Save a numpy-stl Mesh-like object to STL.""" - # mesh_obj is expected to have .save(path) - mesh_obj.save(path) - - -def write_jfh(t_values, r, rot, path: str, mode: str = "1d") -> None: - """Write a JFH file. - - mode="1d" uses print_1d_JFH formatting; mode="generic" uses print_JFH. - """ - if mode == "1d": - print_1d_JFH(t_values, r, rot, path) - else: - print_JFH(t_values, r, rot, path) +""" +IO helpers for RPOD: VTK/STL writers and results directory setup. + +Responsibilities: +- Create case results subdirectories +- Save meshes to STL/VTK +- Write JFH files (wraps util.io.file_print) + +Keep logic thin and parameterized; do not compute physics here. +""" +from __future__ import annotations + +import os +from typing import Any, Sequence + +from pyrpod.util.io.file_print import print_JFH, print_1d_JFH +from pyrpod.util.io.fs import ensure_dir + + +def ensure_results_dirs(case_dir: str, subdirs: Sequence[str] = ("results", "results/strikes", "results/jfh")) -> None: + for sub in subdirs: + ensure_dir(os.path.join(case_dir, sub)) + + +def save_mesh_to_stl(mesh_obj: Any, path: str) -> None: + """Save a numpy-stl Mesh-like object to STL.""" + # mesh_obj is expected to have .save(path) + mesh_obj.save(path) + + +def write_jfh(t_values, r, rot, path: str, mode: str = "1d") -> None: + """Write a JFH file. + + mode="1d" uses print_1d_JFH formatting; mode="generic" uses print_JFH. + """ + if mode == "1d": + print_1d_JFH(t_values, r, rot, path) + else: + print_JFH(t_values, r, rot, path) diff --git a/pyrpod/util/io/fs.py b/pyrpod/util/io/fs.py index 11a9a33..0de1cba 100644 --- a/pyrpod/util/io/fs.py +++ b/pyrpod/util/io/fs.py @@ -1,71 +1,71 @@ -import os -from pathlib import Path - -# pyrpod/util/io/fs.py -> pyrpod/util/io -> pyrpod/util -> pyrpod -> -_REPO_ROOT = Path(__file__).resolve().parents[3] -_SHARED_DATA_DIR = _REPO_ROOT / "data" - -def resolve_asset_path(case_dir, subdir, filename, shared_subdir=None): - """ - Resolve the path to a shared data asset (STL, JFH, TCD, flight plan, ...). - - Checks the case-local copy first (case_dir/subdir/filename) so that - case-specific files always take priority. Falls back to the repo-level - shared `data/shared_subdir/filename` directory when no case-local copy - exists, which is where duplicate assets get consolidated to avoid - carrying multiple copies of the same file across cases. - - Parameters - ---------- - case_dir : str - Path to the case directory (as used elsewhere in pyrpod). - subdir : str - Case-local asset subfolder name, e.g. 'stl', 'jfh', 'tcd'. - filename : str - Name of the asset file. - shared_subdir : str, optional - Asset subfolder name under the shared `data/` directory, if it - differs from `subdir` (e.g. flight plans live under a case's - `jfh/` folder but under `data/flight_plan/`). Defaults to `subdir`. - - Returns - ------- - str - Path to the resolved asset. Prefers the case-local path; falls back - to the shared data directory path even if neither exists, so - callers get a sensible path in error messages. - """ - local_path = os.path.join(case_dir, subdir, filename) - if os.path.exists(local_path): - return local_path - - shared_path = str(_SHARED_DATA_DIR / (shared_subdir or subdir) / filename) - if os.path.exists(shared_path): - return shared_path - - return local_path - -def ensure_dir(path): - """ - Ensure that a directory exists. If it does not exist, create it. - - Parameters - ---------- - path : str - Path to the directory to ensure. - """ - if not os.path.exists(path): - os.makedirs(path) - -def ensure_parent_dir(file_path): - """ - Ensure that the parent directory of a file exists. If it does not exist, create it. - - Parameters - ---------- - file_path : str - Path to the file whose parent directory should be ensured. - """ - parent_dir = os.path.dirname(file_path) - if parent_dir and not os.path.exists(parent_dir): +import os +from pathlib import Path + +# pyrpod/util/io/fs.py -> pyrpod/util/io -> pyrpod/util -> pyrpod -> +_REPO_ROOT = Path(__file__).resolve().parents[3] +_SHARED_DATA_DIR = _REPO_ROOT / "data" + +def resolve_asset_path(case_dir, subdir, filename, shared_subdir=None): + """ + Resolve the path to a shared data asset (STL, JFH, TCD, flight plan, ...). + + Checks the case-local copy first (case_dir/subdir/filename) so that + case-specific files always take priority. Falls back to the repo-level + shared `data/shared_subdir/filename` directory when no case-local copy + exists, which is where duplicate assets get consolidated to avoid + carrying multiple copies of the same file across cases. + + Parameters + ---------- + case_dir : str + Path to the case directory (as used elsewhere in pyrpod). + subdir : str + Case-local asset subfolder name, e.g. 'stl', 'jfh', 'tcd'. + filename : str + Name of the asset file. + shared_subdir : str, optional + Asset subfolder name under the shared `data/` directory, if it + differs from `subdir` (e.g. flight plans live under a case's + `jfh/` folder but under `data/flight_plan/`). Defaults to `subdir`. + + Returns + ------- + str + Path to the resolved asset. Prefers the case-local path; falls back + to the shared data directory path even if neither exists, so + callers get a sensible path in error messages. + """ + local_path = os.path.join(case_dir, subdir, filename) + if os.path.exists(local_path): + return local_path + + shared_path = str(_SHARED_DATA_DIR / (shared_subdir or subdir) / filename) + if os.path.exists(shared_path): + return shared_path + + return local_path + +def ensure_dir(path): + """ + Ensure that a directory exists. If it does not exist, create it. + + Parameters + ---------- + path : str + Path to the directory to ensure. + """ + if not os.path.exists(path): + os.makedirs(path) + +def ensure_parent_dir(file_path): + """ + Ensure that the parent directory of a file exists. If it does not exist, create it. + + Parameters + ---------- + file_path : str + Path to the file whose parent directory should be ensured. + """ + parent_dir = os.path.dirname(file_path) + if parent_dir and not os.path.exists(parent_dir): os.makedirs(parent_dir) \ No newline at end of file diff --git a/pyrpod/util/math/transform.py b/pyrpod/util/math/transform.py index 8948a22..29a5553 100644 --- a/pyrpod/util/math/transform.py +++ b/pyrpod/util/math/transform.py @@ -1,49 +1,49 @@ -import numpy as np - - -def rotation_matrix_from_vectors(vec1: np.ndarray, vec2: np.ndarray) -> np.ndarray: - """ - Compute the 3x3 rotation matrix that rotates vec1 to align with vec2 using - the Rodrigues' rotation formula. - - Parameters - ---------- - vec1 : array_like, shape (3,) - Source vector. - vec2 : array_like, shape (3,) - Destination vector. - - Returns - ------- - rotation_matrix : ndarray, shape (3,3) - Rotation matrix which when applied to vec1 aligns it to vec2. - """ - vec1 = np.asarray(vec1, dtype=float).reshape(3) - vec2 = np.asarray(vec2, dtype=float).reshape(3) - - # Handle identical vectors quickly - if np.allclose(vec1, vec2): - return np.eye(3) - - a = vec1 / np.linalg.norm(vec1) - b = vec2 / np.linalg.norm(vec2) - - v = np.cross(a, b) - c = np.dot(a, b) - s = np.linalg.norm(v) - - # If vectors are opposite, choose an orthogonal axis for 180-degree rotation - if np.isclose(s, 0) and c < 0: - # Find a vector orthogonal to a - ortho = np.array([1.0, 0.0, 0.0]) - if np.allclose(a, ortho): - ortho = np.array([0.0, 1.0, 0.0]) - v = np.cross(a, ortho) - v = v / np.linalg.norm(v) - # Rodrigues for 180 degrees: R = I + 2*K^2 where K is skew-symmetric of v - K = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) - return np.eye(3) + 2 * K.dot(K) - - K = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) - rotation_matrix = np.eye(3) + K + K.dot(K) * ((1 - c) / (s ** 2)) - return rotation_matrix +import numpy as np + + +def rotation_matrix_from_vectors(vec1: np.ndarray, vec2: np.ndarray) -> np.ndarray: + """ + Compute the 3x3 rotation matrix that rotates vec1 to align with vec2 using + the Rodrigues' rotation formula. + + Parameters + ---------- + vec1 : array_like, shape (3,) + Source vector. + vec2 : array_like, shape (3,) + Destination vector. + + Returns + ------- + rotation_matrix : ndarray, shape (3,3) + Rotation matrix which when applied to vec1 aligns it to vec2. + """ + vec1 = np.asarray(vec1, dtype=float).reshape(3) + vec2 = np.asarray(vec2, dtype=float).reshape(3) + + # Handle identical vectors quickly + if np.allclose(vec1, vec2): + return np.eye(3) + + a = vec1 / np.linalg.norm(vec1) + b = vec2 / np.linalg.norm(vec2) + + v = np.cross(a, b) + c = np.dot(a, b) + s = np.linalg.norm(v) + + # If vectors are opposite, choose an orthogonal axis for 180-degree rotation + if np.isclose(s, 0) and c < 0: + # Find a vector orthogonal to a + ortho = np.array([1.0, 0.0, 0.0]) + if np.allclose(a, ortho): + ortho = np.array([0.0, 1.0, 0.0]) + v = np.cross(a, ortho) + v = v / np.linalg.norm(v) + # Rodrigues for 180 degrees: R = I + 2*K^2 where K is skew-symmetric of v + K = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) + return np.eye(3) + 2 * K.dot(K) + + K = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) + rotation_matrix = np.eye(3) + K + K.dot(K) * ((1 - c) / (s ** 2)) + return rotation_matrix diff --git a/pyrpod/util/stl/stl.py b/pyrpod/util/stl/stl.py index 5f76bf8..68d6b51 100644 --- a/pyrpod/util/stl/stl.py +++ b/pyrpod/util/stl/stl.py @@ -1,190 +1,190 @@ -from stl import mesh -import os -from pathlib import Path -import numpy as np -from pyevtk.hl import unstructuredGridToVTK -from pyevtk.vtk import VtkTriangle -from pyrpod.util.io.fs import ensure_parent_dir - -import argparse -import json - - -def load_stl(file_path): - """ - Load an STL file and return a mesh object. - - Parameters - ---------- - file_path : str - Path to the STL file. - - Returns - ------- - mesh.Mesh - The loaded STL mesh object. - """ - if not os.path.exists(file_path): - raise FileNotFoundError(f"STL file not found: {file_path}") - return mesh.Mesh.from_file(file_path) - -def transform_mesh(mesh_obj, rotation_matrix=None, translation_vector=None, scale_factor=None): - """ - Apply transformations to a mesh object. - - Parameters - ---------- - mesh_obj : mesh.Mesh - The mesh object to transform. - rotation_matrix : np.ndarray, optional - A 3x3 rotation matrix to apply to the mesh. - translation_vector : list or np.ndarray, optional - A 3-element vector to translate the mesh. - scale_factor : float, optional - A scaling factor to apply to the mesh. - - Returns - ------- - mesh.Mesh - The transformed mesh object. - """ - if scale_factor: - mesh_obj.points *= scale_factor - if rotation_matrix is not None: - mesh_obj.rotate_using_matrix(rotation_matrix) - if translation_vector is not None: - mesh_obj.translate(translation_vector) - return mesh_obj - - -def transform_mesh_from_file(input_file, output_file, scale, translate): - """ - Transform an STL mesh with scaling and translation and save it to a file. - - Parameters - ---------- - input_file : str - Path to the input STL file. - output_file : str - Path to the output STL file. - scale : float - Scaling factor to apply to the mesh. - translate : list or np.ndarray - Translation vector to apply to the mesh. - """ - # Load the mesh from the file - model = load_stl(input_file) - - # Apply transformations - model = transform_mesh(model, scale_factor=scale, translation_vector=translate) - - # Save the transformed mesh to the output file - model.save(output_file) - print(f"Mesh saved to {output_file}") - - -def convert_stl_to_vtk(surface, out_path, *, filename=None, cellData=None): - """ - Convert an STL mesh (or path to an STL) to a VTK unstructured grid file. - - Parameters - ---------- - surface : stl.mesh.Mesh or str or pathlib.Path - The mesh instance or a path to an STL file. - out_path : str or pathlib.Path - Directory or file path where VTK output should be written. If a directory is - provided, filename will be appended. - filename : str, optional - If provided and out_path is a directory, use this as the base filename - (without extension). If out_path is a file, this is ignored. - cellData : dict of numpy arrays, optional - Optional cell (face) data to attach to the VTK cells. - """ - # Accept either a mesh instance or a path. - if isinstance(surface, (str, Path)): - surface = mesh.Mesh.from_file(str(surface)) - elif not isinstance(surface, mesh.Mesh): - raise TypeError("surface must be stl.mesh.Mesh or path to STL") - - out_path = Path(out_path) - - # If out_path is a directory, build file path from filename or from mesh name - if out_path.is_dir() or out_path.suffix == "": - if filename: - base = filename - else: - base = getattr(surface, 'name', 'mesh') - out_file_stem = out_path / base - else: - out_file_stem = out_path.with_suffix('') - - # Ensure parent directory exists - ensure_parent_dir(str(out_file_stem) + '.vtu') - - # Use numpy operations to build vertex arrays and connectivity - faces = surface.vectors # shape (F,3,3) - n_faces = faces.shape[0] - - coords = faces.reshape(-1, 3) - - # Ensure coordinates are contiguous float arrays (pyevtk requires C/F contiguous) - coords = np.ascontiguousarray(coords, dtype=np.float64) - x = np.ascontiguousarray(coords[:, 0]) - y = np.ascontiguousarray(coords[:, 1]) - z = np.ascontiguousarray(coords[:, 2]) - - conn = np.ascontiguousarray(np.arange(coords.shape[0], dtype=np.int32)) - - offsets = np.ascontiguousarray(np.arange(3, 3 * n_faces + 1, 3, dtype=np.int32)) - - ctype = np.ascontiguousarray(np.full(n_faces, VtkTriangle.tid, dtype=np.uint8)) - - if cellData is None: - cellData = {} - - unstructuredGridToVTK(str(out_file_stem), x, y, z, connectivity=conn, offsets=offsets, cell_types=ctype, cellData=cellData) - - -import argparse -import json -from stl import mesh -import numpy as np - -def transform_mesh(input_file, output_file, scale, translate): - # Load the mesh from the file - model = mesh.Mesh.from_file(input_file) - - # Scale the mesh - model.points *= scale - - # Translate the mesh - model.translate(translate) - - # Save the transformed mesh to the output file - model.save(output_file) - print(f"Mesh saved to {output_file}") - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Transform an STL mesh with scaling and translation using a configuration file.") - - # Configuration file - parser.add_argument("config_file", type=str, help="Path to the configuration JSON file.") - - args = parser.parse_args() - - # Load configuration from the file - with open(args.config_file, 'r') as config_file: - config = json.load(config_file) - - input_file = config.get("input_file") - output_file = config.get("output_file") - scale = config.get("scale", 1.0) - translate = config.get("translate", [0.0, 0.0, 0.0]) - - # Transform the mesh using the configuration - transform_mesh( - input_file=input_file, - output_file=output_file, - scale=scale, - translate=translate - ) +from stl import mesh +import os +from pathlib import Path +import numpy as np +from pyevtk.hl import unstructuredGridToVTK +from pyevtk.vtk import VtkTriangle +from pyrpod.util.io.fs import ensure_parent_dir + +import argparse +import json + + +def load_stl(file_path): + """ + Load an STL file and return a mesh object. + + Parameters + ---------- + file_path : str + Path to the STL file. + + Returns + ------- + mesh.Mesh + The loaded STL mesh object. + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"STL file not found: {file_path}") + return mesh.Mesh.from_file(file_path) + +def transform_mesh(mesh_obj, rotation_matrix=None, translation_vector=None, scale_factor=None): + """ + Apply transformations to a mesh object. + + Parameters + ---------- + mesh_obj : mesh.Mesh + The mesh object to transform. + rotation_matrix : np.ndarray, optional + A 3x3 rotation matrix to apply to the mesh. + translation_vector : list or np.ndarray, optional + A 3-element vector to translate the mesh. + scale_factor : float, optional + A scaling factor to apply to the mesh. + + Returns + ------- + mesh.Mesh + The transformed mesh object. + """ + if scale_factor: + mesh_obj.points *= scale_factor + if rotation_matrix is not None: + mesh_obj.rotate_using_matrix(rotation_matrix) + if translation_vector is not None: + mesh_obj.translate(translation_vector) + return mesh_obj + + +def transform_mesh_from_file(input_file, output_file, scale, translate): + """ + Transform an STL mesh with scaling and translation and save it to a file. + + Parameters + ---------- + input_file : str + Path to the input STL file. + output_file : str + Path to the output STL file. + scale : float + Scaling factor to apply to the mesh. + translate : list or np.ndarray + Translation vector to apply to the mesh. + """ + # Load the mesh from the file + model = load_stl(input_file) + + # Apply transformations + model = transform_mesh(model, scale_factor=scale, translation_vector=translate) + + # Save the transformed mesh to the output file + model.save(output_file) + print(f"Mesh saved to {output_file}") + + +def convert_stl_to_vtk(surface, out_path, *, filename=None, cellData=None): + """ + Convert an STL mesh (or path to an STL) to a VTK unstructured grid file. + + Parameters + ---------- + surface : stl.mesh.Mesh or str or pathlib.Path + The mesh instance or a path to an STL file. + out_path : str or pathlib.Path + Directory or file path where VTK output should be written. If a directory is + provided, filename will be appended. + filename : str, optional + If provided and out_path is a directory, use this as the base filename + (without extension). If out_path is a file, this is ignored. + cellData : dict of numpy arrays, optional + Optional cell (face) data to attach to the VTK cells. + """ + # Accept either a mesh instance or a path. + if isinstance(surface, (str, Path)): + surface = mesh.Mesh.from_file(str(surface)) + elif not isinstance(surface, mesh.Mesh): + raise TypeError("surface must be stl.mesh.Mesh or path to STL") + + out_path = Path(out_path) + + # If out_path is a directory, build file path from filename or from mesh name + if out_path.is_dir() or out_path.suffix == "": + if filename: + base = filename + else: + base = getattr(surface, 'name', 'mesh') + out_file_stem = out_path / base + else: + out_file_stem = out_path.with_suffix('') + + # Ensure parent directory exists + ensure_parent_dir(str(out_file_stem) + '.vtu') + + # Use numpy operations to build vertex arrays and connectivity + faces = surface.vectors # shape (F,3,3) + n_faces = faces.shape[0] + + coords = faces.reshape(-1, 3) + + # Ensure coordinates are contiguous float arrays (pyevtk requires C/F contiguous) + coords = np.ascontiguousarray(coords, dtype=np.float64) + x = np.ascontiguousarray(coords[:, 0]) + y = np.ascontiguousarray(coords[:, 1]) + z = np.ascontiguousarray(coords[:, 2]) + + conn = np.ascontiguousarray(np.arange(coords.shape[0], dtype=np.int32)) + + offsets = np.ascontiguousarray(np.arange(3, 3 * n_faces + 1, 3, dtype=np.int32)) + + ctype = np.ascontiguousarray(np.full(n_faces, VtkTriangle.tid, dtype=np.uint8)) + + if cellData is None: + cellData = {} + + unstructuredGridToVTK(str(out_file_stem), x, y, z, connectivity=conn, offsets=offsets, cell_types=ctype, cellData=cellData) + + +import argparse +import json +from stl import mesh +import numpy as np + +def transform_mesh(input_file, output_file, scale, translate): + # Load the mesh from the file + model = mesh.Mesh.from_file(input_file) + + # Scale the mesh + model.points *= scale + + # Translate the mesh + model.translate(translate) + + # Save the transformed mesh to the output file + model.save(output_file) + print(f"Mesh saved to {output_file}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Transform an STL mesh with scaling and translation using a configuration file.") + + # Configuration file + parser.add_argument("config_file", type=str, help="Path to the configuration JSON file.") + + args = parser.parse_args() + + # Load configuration from the file + with open(args.config_file, 'r') as config_file: + config = json.load(config_file) + + input_file = config.get("input_file") + output_file = config.get("output_file") + scale = config.get("scale", 1.0) + translate = config.get("translate", [0.0, 0.0, 0.0]) + + # Transform the mesh using the configuration + transform_mesh( + input_file=input_file, + output_file=output_file, + scale=scale, + translate=translate + ) diff --git a/pyrpod/util/stl/transform_stl.json b/pyrpod/util/stl/transform_stl.json index 0c8ab67..88f51ef 100644 --- a/pyrpod/util/stl/transform_stl.json +++ b/pyrpod/util/stl/transform_stl.json @@ -1,6 +1,6 @@ -{ - "input_file": "cylinder.STL", - "output_file": "cylinder_transformed.STL", - "scale": 0.001, - "translate": [-7, -2, -2] -} +{ + "input_file": "cylinder.STL", + "output_file": "cylinder_transformed.STL", + "scale": 0.001, + "translate": [-7, -2, -2] +} diff --git a/pyrpod/vehicle/LogisticsModule.py b/pyrpod/vehicle/LogisticsModule.py index e6c6dbf..b559353 100644 --- a/pyrpod/vehicle/LogisticsModule.py +++ b/pyrpod/vehicle/LogisticsModule.py @@ -1,417 +1,417 @@ -from pyrpod.vehicle.VisitingVehicle import VisitingVehicle -from stl import mesh -import numpy as np -from matplotlib import pyplot as plt -from mpl_toolkits import mplot3d -import os -import configparser -from pyrpod.logging_utils import get_logger -from pyrpod.util.io.fs import resolve_asset_path - -logger = get_logger("pyrpod.vehicle.LogisticsModule") - -class LogisticsModule(VisitingVehicle): - """ - Extends the Visiting Vehicle object to consider RCS working groups. - (Is this class necessary? Can this functionality be kept in the VV object?) - - Attributes - ---------- - mass : float - Docking (target) mass for LM - - height : float - LM height (cylinder form factor) - - radius : float - LM radius (cylinder form factor) - - volume : float - LM volume (cylinder form factor) - - I_x : float - x-axis (roll) moment of inertia (cylinder form factor) - - I_y : float - y-axis (pitch) moment of inertia (cylinder form factor) - - I_z : float - z-axis (yaw) moment of inertia (cylinder form factor) - - Methods - ------- - add_thruster_performance(thrust_val, isp) - Assigns thruster performance using thruster ID specified in TCD file - - calc_thruster_performance() - Calculates performance of each thruster fired inidividually. - - rcs_group_str_to_list(group) - Helper method needed convert configuration data into a list. WIP - - print_rcs_groups() - Simple method to format printing of RCS groups - - assign_thrusters(group) - Assigns RCS thrusters to working groups. - - assign_thruster_groups() - Wrapper method for grouping RCS thruster according to provided configuration data. - - plot_active_thrusters(active_thrusters, group, normals) - Plots active thrusters for a specified working group. - - plot_thruster_group(group) - Wrapper method to plot active thrusters in a given working group. - - check_thruster_groups() - Plots all thruster working groups in the RCS configuration. - - """ - # TODO: write a custom COM of calculator for comparing RCS configurations (method) - def __init__(self, case_dir): - """ - Class responsible for handling visiting vehicle data. - - Includes surface mesh and thruster configuration data. - - NOTE: Is this redundant? Can we use the constructor specified in Vehicle.py? - Answer is proably yes, but we need to test this. - - Attributes - ---------- - config : ConfigParser - Object responsible for reading data from the provided configuration file. - - case_dir : str - Path to case directory. Used to store data and results for a specific scenario. - - Methods - ------- - convert_stl_to_vtk(cellData, mesh) - Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. - """ - # Delegate initialization to parent to avoid duplicating config reading - super().__init__(case_dir) - - def set_inertial_props(self, mass, height, radius): - """ - Simple constructor used to establish LM inertial properties. - - Parameters - ---------- - mass : float - Mass for the logistics module. Early calculations assume GDSS max docking mass. - - height : float - Height for the LM geometry, which is assumed to be a cylinder. - - radius : float - Radius for the LM geometry, which is assumed to be a cylinder. - - Returns - ------- - Method doesn't currently return anything. Simply assigns class members as needed. - Does the method need to return a status message? or pass similar data? - """ - - # TODO: Add center of mass information. - - # Store provided data. - self.mass = mass - self.height = height - self.radius = radius - - # Calculate volume for a cylinder. - self.volume = height * 3.14 * radius **2 - - # Calculate moments of inertia. - self.I_x = 0.5*mass*radius**2 - self.I_y = (1.0/12.0)*mass*(height**2 + 3*radius**2) - self.I_z = self.I_y - - return - - def add_thruster_performance(self, thrust_val, isp): - """ WIP. Assigns thruster performance characteristics using thruster ID specified in TCD file.""" - # TODO: re-write method to read in data from CSV file. Do docstring after. - # 1. mass, 2. chamber temp, 3. chamber pressure 4. velocity 5. impulse bit - # 6. thruster id, 7. gas composition, - self.thrust = thrust_val - self.isp = isp - return - - def calc_thruster_performance(self): - """ - Calculates performance of each thruster fired individually. - - This simple method ignores changes in propellant mass, which should be addressed in future iterations. - Returns a list of dictionaries containing performance data for each thruster. - """ - # TODO: Add similar methods that include fuel usage, self-impingement, cant angle sweep, and vector analysis. - thruster_performance_data = [] - - for thruster_id, thruster_info in self.thruster_data.items(): - # Select current thruster from dictionary - cur_thruster = thruster_info - - # Extract the normal vector - dcm = cur_thruster['dcm'] - n = [dcm[0][2], dcm[1][2], dcm[2][2]] - - # Calculate thruster force vector - F_thruster = -1 * np.array(n) * self.thrust - - # Calculate acceleration performance - a_x = round(F_thruster[0] / self.mass, 3) - a_y = round(F_thruster[1] / self.mass, 3) - a_z = round(F_thruster[2] / self.mass, 3) - translational_acceleration = np.array([a_x, a_y, a_z]) - - # Calculate torque vector - r = cur_thruster['exit'][0] # Thruster position vector - T_x = F_thruster[1] * r[2] + F_thruster[2] * r[1] - T_y = F_thruster[0] * r[2] + F_thruster[2] * r[0] - T_z = F_thruster[0] * r[1] + F_thruster[1] * r[0] - torque = np.array([T_x, T_y, T_z]) / self.I_x # Rotational acceleration - - # Store calculated data in a dictionary for this thruster - thruster_data = { - 'thruster_id': thruster_id, - 'normal_vector': n, - 'force_vector': F_thruster, - 'translational_acceleration': translational_acceleration, - 'torque': torque - } - - thruster_performance_data.append(thruster_data) - - return thruster_performance_data - - def rcs_group_str_to_list(self, working_group): - """ - Helper method needed convert configuration data into a list. - - Parameters - ---------- - working_group : str - String to ID RCS working group according to directionality of motion. - - Returns - ------- - group_list : list - contains thruster_ids for the specified RCS working group. - - """ - - # AKA: the config method I used is janky af but will work for the immediate future. - # TODO: Need to consider alternative data structures. This function might be deleted in that process. - - group_str = self.config['thruster_groups'][working_group] - group_str = group_str.strip('[') - group_str = group_str.strip(']') - - group_list = group_str.split(',') - - for i, string in enumerate(group_list): - - group_list[i] = group_list[i].strip() - group_list[i] = group_list[i].strip("'") - - return group_list - - def print_rcs_groups(self): - """Simple method to format printing of RCS groups""" - for group in self.rcs_groups: - logger.info("%s %s", group, self.rcs_groups[group]) - - def assign_thrusters(self, group): - """ - Assigns RCS thrusters to a specificed working group - - Parameters - ---------- - group : str - RCS working group. Needs better name? - - normals: 2d list - Normal vectors for plume cone center line. WIP not being used as of now. - - Returns - ------- - Method doesn't currently return anything. Simply assigns class members as needed. - Does the method need to return a status message? or pass similar data? - """ - thruster_ids = self.rcs_group_str_to_list(group) - - self.rcs_groups[group] = [] - - for thruster in thruster_ids: - self.rcs_groups[group].append(thruster) - # print(self.rcs_groups) - - def assign_thruster_groups(self): - """Wrapper method for grouping RCS thrusters according to provided configuration data.""" - - # Read in grouping configuration file. - config = configparser.ConfigParser() - try: - config.read(resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['tgf'])) - except KeyError: - # print("WARNING: Thruster Grouping File Not Set") - self.rcs_groups = None - return - - - self.config = config - - #Instantiate dictionary to hold grouping info - self.rcs_groups = {} - - # printer thruster data (for reference) - # for thruster in self.thruster_data: - # # print(self.thruster_data[thruster]) - - - # Collect labels for rcs groups (x/y/z and roll/pitch/yaw rates) - group_ids = [] - for item in self.config.items('thruster_groups'): - group_ids.append(item[0]) - - # Assign thruster groups according provided grouping data. - for group in group_ids: - self.assign_thrusters(group) - - decel_thruster_name = next(iter(self.rcs_groups['neg_x'])) - self.decel_cant = self.get_thruster_cant(decel_thruster_name) - - def plot_active_thrusters(self, active_thrusters, working_group, normals): - """ - Plots active thrusters for a specified working group. - - Parameters - ---------- - active_thrusters : mesh.Mesh - STL mesh containing tranformed cones of all active thrusters. - - working_group : str - String to ID RCS working group according to directionality of motion. - - normals: 2d list - Normal vectors for plume cone center line. WIP not being used as of now. - - Returns - ------- - Method doesn't currently return anything. Simply saves plots as image. - Does the method need to return a status message? or pass similar data? - """ - - # Save STL for VV into a local variable. (readability) - VVmesh = self.mesh - - # Instantiate object to hold visual plots. - figure = plt.figure() - axes = figure.add_subplot(projection = '3d') - - # Add STL files for VV and active plumes to plot. - axes.add_collection3d(mplot3d.art3d.Poly3DCollection(VVmesh.vectors)) - - # Change color of Plume STL first - surface = mplot3d.art3d.Poly3DCollection(active_thrusters.vectors) - surface.set_facecolor('orange') - axes.add_collection3d(surface) - - # Add normal vector for RCS plume centerlines. - # axes.quiver(normal[0], normal[1], normal[2], normal[3], normal[4], normal[5], color = (0,0,0), length=4, normalize=True) - - # Set view port limits - lim = 7 - axes.set_xlim([-1*lim - 3, lim - 3]) - axes.set_ylim([-1*lim, lim]) - axes.set_zlim([-1*lim, lim]) - - # Set labels - axes.set_xlabel('X') - axes.set_ylabel('Y') - axes.set_zlabel('Z') - figure.suptitle(working_group) - - # Save to file - plt.savefig('img/frame' + str(working_group) + '.png') - - def plot_thruster_group(self, working_group): - """ - Wrapper method to plot active thrusters in a given working group. Name is confusing need to revise. - - Parameters - ---------- - working_group : str - String to ID RCS working group according to directionality of motion. - - Returns - ------- - Method doesn't currently return anything. Simply saves stl files as needed. - Does the method need to return a status message? or pass similar data? - """ - active_thrusters = None - normals = [] - - # Initiate and plot all active thrusters in the group - for thruster in self.rcs_groups[working_group]: - - plumeMesh = self.initiate_plume_mesh() - plumeMesh = self.transform_plume_mesh(thruster, plumeMesh) - normals.append(self.initiate_plume_normal(thruster)) - - if active_thrusters == None: - active_thrusters = plumeMesh - else: - active_thrusters = mesh.Mesh(np.concatenate([active_thrusters.data, plumeMesh.data])) - - if not os.path.isdir('stl/groups/'): - os.system('mkdir stl/groups') - - active_thrusters.save('stl/groups/' + working_group + '.stl') - - self.plot_active_thrusters(active_thrusters, working_group, normals) - - def check_thruster_groups(self): - """ - Plots all thruster working groups in the RCS configuration. - - Is essentially a wrapper method for the wrapper method. Yikes. - """ - self.print_rcs_groups() - - for group in self.rcs_groups: - # print(group) - self.plot_thruster_group(group) - # print() - return - - def calc_overshoot_v_range(self, v_ida, r_o): - mass = self.mass - # print(len(self.rcs_groups['neg_x'])) - - F_decel = 0 - for thruster in self.rcs_groups['neg_x']: - thruster_type = self.thruster_data[thruster]['type'][0] - thruster_metrics = self.thruster_metrics[thruster_type] - - cant = self.decel_cant - - F_decel += (thruster_metrics['F'] * np.cos(cant)) - - logger.debug("F_decel computed: %s", F_decel) - a_decel = F_decel / mass - - v_o = np.sqrt(v_ida**2 + 2 * a_decel * r_o) - logger.debug("v_o computed: %s", v_o) - - vo_range = [0.1*v_o, 0.25*v_o, 0.5*v_o, 0.75*v_o, v_o] - logger.debug("vo_range: %s", vo_range) - return vo_range - - def debug_decel_calc_example(self, F_decel, v_o, vo_range): - # Replaces ad-hoc prints with a single debug helper (optional usage) +from pyrpod.vehicle.VisitingVehicle import VisitingVehicle +from stl import mesh +import numpy as np +from matplotlib import pyplot as plt +from mpl_toolkits import mplot3d +import os +import configparser +from pyrpod.logging_utils import get_logger +from pyrpod.util.io.fs import resolve_asset_path + +logger = get_logger("pyrpod.vehicle.LogisticsModule") + +class LogisticsModule(VisitingVehicle): + """ + Extends the Visiting Vehicle object to consider RCS working groups. + (Is this class necessary? Can this functionality be kept in the VV object?) + + Attributes + ---------- + mass : float + Docking (target) mass for LM + + height : float + LM height (cylinder form factor) + + radius : float + LM radius (cylinder form factor) + + volume : float + LM volume (cylinder form factor) + + I_x : float + x-axis (roll) moment of inertia (cylinder form factor) + + I_y : float + y-axis (pitch) moment of inertia (cylinder form factor) + + I_z : float + z-axis (yaw) moment of inertia (cylinder form factor) + + Methods + ------- + add_thruster_performance(thrust_val, isp) + Assigns thruster performance using thruster ID specified in TCD file + + calc_thruster_performance() + Calculates performance of each thruster fired inidividually. + + rcs_group_str_to_list(group) + Helper method needed convert configuration data into a list. WIP + + print_rcs_groups() + Simple method to format printing of RCS groups + + assign_thrusters(group) + Assigns RCS thrusters to working groups. + + assign_thruster_groups() + Wrapper method for grouping RCS thruster according to provided configuration data. + + plot_active_thrusters(active_thrusters, group, normals) + Plots active thrusters for a specified working group. + + plot_thruster_group(group) + Wrapper method to plot active thrusters in a given working group. + + check_thruster_groups() + Plots all thruster working groups in the RCS configuration. + + """ + # TODO: write a custom COM of calculator for comparing RCS configurations (method) + def __init__(self, case_dir): + """ + Class responsible for handling visiting vehicle data. + + Includes surface mesh and thruster configuration data. + + NOTE: Is this redundant? Can we use the constructor specified in Vehicle.py? + Answer is proably yes, but we need to test this. + + Attributes + ---------- + config : ConfigParser + Object responsible for reading data from the provided configuration file. + + case_dir : str + Path to case directory. Used to store data and results for a specific scenario. + + Methods + ------- + convert_stl_to_vtk(cellData, mesh) + Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. + """ + # Delegate initialization to parent to avoid duplicating config reading + super().__init__(case_dir) + + def set_inertial_props(self, mass, height, radius): + """ + Simple constructor used to establish LM inertial properties. + + Parameters + ---------- + mass : float + Mass for the logistics module. Early calculations assume GDSS max docking mass. + + height : float + Height for the LM geometry, which is assumed to be a cylinder. + + radius : float + Radius for the LM geometry, which is assumed to be a cylinder. + + Returns + ------- + Method doesn't currently return anything. Simply assigns class members as needed. + Does the method need to return a status message? or pass similar data? + """ + + # TODO: Add center of mass information. + + # Store provided data. + self.mass = mass + self.height = height + self.radius = radius + + # Calculate volume for a cylinder. + self.volume = height * 3.14 * radius **2 + + # Calculate moments of inertia. + self.I_x = 0.5*mass*radius**2 + self.I_y = (1.0/12.0)*mass*(height**2 + 3*radius**2) + self.I_z = self.I_y + + return + + def add_thruster_performance(self, thrust_val, isp): + """ WIP. Assigns thruster performance characteristics using thruster ID specified in TCD file.""" + # TODO: re-write method to read in data from CSV file. Do docstring after. + # 1. mass, 2. chamber temp, 3. chamber pressure 4. velocity 5. impulse bit + # 6. thruster id, 7. gas composition, + self.thrust = thrust_val + self.isp = isp + return + + def calc_thruster_performance(self): + """ + Calculates performance of each thruster fired individually. + + This simple method ignores changes in propellant mass, which should be addressed in future iterations. + Returns a list of dictionaries containing performance data for each thruster. + """ + # TODO: Add similar methods that include fuel usage, self-impingement, cant angle sweep, and vector analysis. + thruster_performance_data = [] + + for thruster_id, thruster_info in self.thruster_data.items(): + # Select current thruster from dictionary + cur_thruster = thruster_info + + # Extract the normal vector + dcm = cur_thruster['dcm'] + n = [dcm[0][2], dcm[1][2], dcm[2][2]] + + # Calculate thruster force vector + F_thruster = -1 * np.array(n) * self.thrust + + # Calculate acceleration performance + a_x = round(F_thruster[0] / self.mass, 3) + a_y = round(F_thruster[1] / self.mass, 3) + a_z = round(F_thruster[2] / self.mass, 3) + translational_acceleration = np.array([a_x, a_y, a_z]) + + # Calculate torque vector + r = cur_thruster['exit'][0] # Thruster position vector + T_x = F_thruster[1] * r[2] + F_thruster[2] * r[1] + T_y = F_thruster[0] * r[2] + F_thruster[2] * r[0] + T_z = F_thruster[0] * r[1] + F_thruster[1] * r[0] + torque = np.array([T_x, T_y, T_z]) / self.I_x # Rotational acceleration + + # Store calculated data in a dictionary for this thruster + thruster_data = { + 'thruster_id': thruster_id, + 'normal_vector': n, + 'force_vector': F_thruster, + 'translational_acceleration': translational_acceleration, + 'torque': torque + } + + thruster_performance_data.append(thruster_data) + + return thruster_performance_data + + def rcs_group_str_to_list(self, working_group): + """ + Helper method needed convert configuration data into a list. + + Parameters + ---------- + working_group : str + String to ID RCS working group according to directionality of motion. + + Returns + ------- + group_list : list + contains thruster_ids for the specified RCS working group. + + """ + + # AKA: the config method I used is janky af but will work for the immediate future. + # TODO: Need to consider alternative data structures. This function might be deleted in that process. + + group_str = self.config['thruster_groups'][working_group] + group_str = group_str.strip('[') + group_str = group_str.strip(']') + + group_list = group_str.split(',') + + for i, string in enumerate(group_list): + + group_list[i] = group_list[i].strip() + group_list[i] = group_list[i].strip("'") + + return group_list + + def print_rcs_groups(self): + """Simple method to format printing of RCS groups""" + for group in self.rcs_groups: + logger.info("%s %s", group, self.rcs_groups[group]) + + def assign_thrusters(self, group): + """ + Assigns RCS thrusters to a specificed working group + + Parameters + ---------- + group : str + RCS working group. Needs better name? + + normals: 2d list + Normal vectors for plume cone center line. WIP not being used as of now. + + Returns + ------- + Method doesn't currently return anything. Simply assigns class members as needed. + Does the method need to return a status message? or pass similar data? + """ + thruster_ids = self.rcs_group_str_to_list(group) + + self.rcs_groups[group] = [] + + for thruster in thruster_ids: + self.rcs_groups[group].append(thruster) + # print(self.rcs_groups) + + def assign_thruster_groups(self): + """Wrapper method for grouping RCS thrusters according to provided configuration data.""" + + # Read in grouping configuration file. + config = configparser.ConfigParser() + try: + config.read(resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['tgf'])) + except KeyError: + # print("WARNING: Thruster Grouping File Not Set") + self.rcs_groups = None + return + + + self.config = config + + #Instantiate dictionary to hold grouping info + self.rcs_groups = {} + + # printer thruster data (for reference) + # for thruster in self.thruster_data: + # # print(self.thruster_data[thruster]) + + + # Collect labels for rcs groups (x/y/z and roll/pitch/yaw rates) + group_ids = [] + for item in self.config.items('thruster_groups'): + group_ids.append(item[0]) + + # Assign thruster groups according provided grouping data. + for group in group_ids: + self.assign_thrusters(group) + + decel_thruster_name = next(iter(self.rcs_groups['neg_x'])) + self.decel_cant = self.get_thruster_cant(decel_thruster_name) + + def plot_active_thrusters(self, active_thrusters, working_group, normals): + """ + Plots active thrusters for a specified working group. + + Parameters + ---------- + active_thrusters : mesh.Mesh + STL mesh containing tranformed cones of all active thrusters. + + working_group : str + String to ID RCS working group according to directionality of motion. + + normals: 2d list + Normal vectors for plume cone center line. WIP not being used as of now. + + Returns + ------- + Method doesn't currently return anything. Simply saves plots as image. + Does the method need to return a status message? or pass similar data? + """ + + # Save STL for VV into a local variable. (readability) + VVmesh = self.mesh + + # Instantiate object to hold visual plots. + figure = plt.figure() + axes = figure.add_subplot(projection = '3d') + + # Add STL files for VV and active plumes to plot. + axes.add_collection3d(mplot3d.art3d.Poly3DCollection(VVmesh.vectors)) + + # Change color of Plume STL first + surface = mplot3d.art3d.Poly3DCollection(active_thrusters.vectors) + surface.set_facecolor('orange') + axes.add_collection3d(surface) + + # Add normal vector for RCS plume centerlines. + # axes.quiver(normal[0], normal[1], normal[2], normal[3], normal[4], normal[5], color = (0,0,0), length=4, normalize=True) + + # Set view port limits + lim = 7 + axes.set_xlim([-1*lim - 3, lim - 3]) + axes.set_ylim([-1*lim, lim]) + axes.set_zlim([-1*lim, lim]) + + # Set labels + axes.set_xlabel('X') + axes.set_ylabel('Y') + axes.set_zlabel('Z') + figure.suptitle(working_group) + + # Save to file + plt.savefig('img/frame' + str(working_group) + '.png') + + def plot_thruster_group(self, working_group): + """ + Wrapper method to plot active thrusters in a given working group. Name is confusing need to revise. + + Parameters + ---------- + working_group : str + String to ID RCS working group according to directionality of motion. + + Returns + ------- + Method doesn't currently return anything. Simply saves stl files as needed. + Does the method need to return a status message? or pass similar data? + """ + active_thrusters = None + normals = [] + + # Initiate and plot all active thrusters in the group + for thruster in self.rcs_groups[working_group]: + + plumeMesh = self.initiate_plume_mesh() + plumeMesh = self.transform_plume_mesh(thruster, plumeMesh) + normals.append(self.initiate_plume_normal(thruster)) + + if active_thrusters == None: + active_thrusters = plumeMesh + else: + active_thrusters = mesh.Mesh(np.concatenate([active_thrusters.data, plumeMesh.data])) + + if not os.path.isdir('stl/groups/'): + os.system('mkdir stl/groups') + + active_thrusters.save('stl/groups/' + working_group + '.stl') + + self.plot_active_thrusters(active_thrusters, working_group, normals) + + def check_thruster_groups(self): + """ + Plots all thruster working groups in the RCS configuration. + + Is essentially a wrapper method for the wrapper method. Yikes. + """ + self.print_rcs_groups() + + for group in self.rcs_groups: + # print(group) + self.plot_thruster_group(group) + # print() + return + + def calc_overshoot_v_range(self, v_ida, r_o): + mass = self.mass + # print(len(self.rcs_groups['neg_x'])) + + F_decel = 0 + for thruster in self.rcs_groups['neg_x']: + thruster_type = self.thruster_data[thruster]['type'][0] + thruster_metrics = self.thruster_metrics[thruster_type] + + cant = self.decel_cant + + F_decel += (thruster_metrics['F'] * np.cos(cant)) + + logger.debug("F_decel computed: %s", F_decel) + a_decel = F_decel / mass + + v_o = np.sqrt(v_ida**2 + 2 * a_decel * r_o) + logger.debug("v_o computed: %s", v_o) + + vo_range = [0.1*v_o, 0.25*v_o, 0.5*v_o, 0.75*v_o, v_o] + logger.debug("vo_range: %s", vo_range) + return vo_range + + def debug_decel_calc_example(self, F_decel, v_o, vo_range): + # Replaces ad-hoc prints with a single debug helper (optional usage) logger.debug("F_decel=%s, v_o=%s, vo_range=%s", F_decel, v_o, vo_range) \ No newline at end of file diff --git a/pyrpod/vehicle/TargetVehicle.py b/pyrpod/vehicle/TargetVehicle.py index 660b7f6..b4e74ad 100644 --- a/pyrpod/vehicle/TargetVehicle.py +++ b/pyrpod/vehicle/TargetVehicle.py @@ -1,66 +1,66 @@ -from stl import mesh -from pyrpod.vehicle.Vehicle import Vehicle -from pyrpod.util.io.fs import resolve_asset_path - -class TargetVehicle(Vehicle): - """ - Class responsible for handling visiting vehicle data. - - Includes surface mesh and thruster configuration data. - - Attributes - ---------- - Includes attributes defined in parent class "Vehicle". - - path_to_stl : str - file location for Vehicle's surface mesh using an STL file. - - mesh : stl.mesh.Mesh - Contains surface mesh using data read from STL file. - - Methods - ------- - set_stl() - Read in thruster configuration data from the provided file path. - - set_stl_elements() - Read in thruster configuration data from the provided file path. - - """ - - def set_stl(self): - """ - Reads in Vehicle surface mesh from STL file. - - Parameters - ---------- - path_to_stl : str - file location for Vehicle's surface mesh using an STL file. - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - """ - path_to_stl = resolve_asset_path(self.case_dir, 'stl', self.config['tv']['stl']) - meshes = mesh.Mesh.from_multi_file(path_to_stl) - self.mesh = next(meshes) - #self.mesh = next(meshes) - self.path_to_stl = path_to_stl - return - - def set_stl_elements(self): - """ - place holder method now. A strech goal could be to - load in a multi surface stl file which accounts for - different componoents of the Gateway to impinge upon. - - """ - print('') - return - - def set_v_ida(self, v_ida): - self.v_ida = v_ida - - def set_r_o(self, r_o): +from stl import mesh +from pyrpod.vehicle.Vehicle import Vehicle +from pyrpod.util.io.fs import resolve_asset_path + +class TargetVehicle(Vehicle): + """ + Class responsible for handling visiting vehicle data. + + Includes surface mesh and thruster configuration data. + + Attributes + ---------- + Includes attributes defined in parent class "Vehicle". + + path_to_stl : str + file location for Vehicle's surface mesh using an STL file. + + mesh : stl.mesh.Mesh + Contains surface mesh using data read from STL file. + + Methods + ------- + set_stl() + Read in thruster configuration data from the provided file path. + + set_stl_elements() + Read in thruster configuration data from the provided file path. + + """ + + def set_stl(self): + """ + Reads in Vehicle surface mesh from STL file. + + Parameters + ---------- + path_to_stl : str + file location for Vehicle's surface mesh using an STL file. + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + """ + path_to_stl = resolve_asset_path(self.case_dir, 'stl', self.config['tv']['stl']) + meshes = mesh.Mesh.from_multi_file(path_to_stl) + self.mesh = next(meshes) + #self.mesh = next(meshes) + self.path_to_stl = path_to_stl + return + + def set_stl_elements(self): + """ + place holder method now. A strech goal could be to + load in a multi surface stl file which accounts for + different componoents of the Gateway to impinge upon. + + """ + print('') + return + + def set_v_ida(self, v_ida): + self.v_ida = v_ida + + def set_r_o(self, r_o): self.r_o = r_o \ No newline at end of file diff --git a/pyrpod/vehicle/Vehicle.py b/pyrpod/vehicle/Vehicle.py index e13e3ab..3cd1edc 100644 --- a/pyrpod/vehicle/Vehicle.py +++ b/pyrpod/vehicle/Vehicle.py @@ -1,105 +1,105 @@ -from stl import mesh -import numpy as np -import os -import configparser - -from pyevtk.vtk import VtkTriangle, VtkQuad -from pyrpod.util.stl.stl import convert_stl_to_vtk -from pyrpod.util.io.fs import resolve_asset_path - -class Vehicle: - """ - Class responsible for handling visiting vehicle data. - - Includes surface mesh and thruster configuration data. - - Attributes - ---------- - config : ConfigParser - Object responsible for reading data from the provided configuration file. - - case_dir : str - Path to case directory. Used to store data and results for a specific scenario. - - Methods - ------- - convert_stl_to_vtk(cellData, mesh) - Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. - """ - def __init__(self, case_dir): - self.case_dir = case_dir - config = configparser.ConfigParser() - config.read(self.case_dir + "config.ini") - self.config = config - - def set_stl(self): - """ - Reads in Vehicle surface mesh from STL file. - - Parameters - ---------- - path_to_stl : str - file location for Vehicle's surface mesh using an STL file. - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - """ - path_to_stl = resolve_asset_path(self.case_dir, 'stl', self.config['vv']['stl_lm']) - - self.mesh = mesh.Mesh.from_file(path_to_stl) - self.path_to_stl = path_to_stl - return - - def convert_stl_to_vtk_strikes(self, path_to_vtk, cellData, mesh): - """ - Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. - - Parameters - ---------- - cellData : dict - Dictionary storing arrays that contain all surface properties. - - Returns - ------- - Method doesn't currently return anything. Simply saves data to files as needed. - Does the method need to return a status message? or pass similar data? - """ - - # if self.mesh == None and mesh == None: - # print("mesh is not set. Please load using self.set_stl() method") - # return - - surface = self.mesh if mesh is None else mesh - # Ensure output directory exists and derive base filename - out_dir = self.case_dir + 'results/' - convert_stl_to_vtk(surface, out_dir, filename=path_to_vtk, cellData=cellData) - return - - - def convert_stl_to_vtk(self): - """ - Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. - - Parameters - ---------- - cellData : dict - Dictionary storing arrays that contain all surface properties. - - Returns - ------- - Method doesn't currently return anything. Simply saves data to files as needed. - Does the method need to return a status message? or pass similar data? - """ - - surface = self.mesh - out_dir = self.case_dir + 'results/' - # Default cell data - cellData = {"strikes": np.zeros(len(surface.vectors))} - # Derive filename from path_to_stl if available - filename = None - if hasattr(self, 'path_to_stl') and self.path_to_stl: - filename = self.path_to_stl.split('/')[-1].split('.')[0] - convert_stl_to_vtk(surface, out_dir, filename=filename, cellData=cellData) +from stl import mesh +import numpy as np +import os +import configparser + +from pyevtk.vtk import VtkTriangle, VtkQuad +from pyrpod.util.stl.stl import convert_stl_to_vtk +from pyrpod.util.io.fs import resolve_asset_path + +class Vehicle: + """ + Class responsible for handling visiting vehicle data. + + Includes surface mesh and thruster configuration data. + + Attributes + ---------- + config : ConfigParser + Object responsible for reading data from the provided configuration file. + + case_dir : str + Path to case directory. Used to store data and results for a specific scenario. + + Methods + ------- + convert_stl_to_vtk(cellData, mesh) + Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. + """ + def __init__(self, case_dir): + self.case_dir = case_dir + config = configparser.ConfigParser() + config.read(self.case_dir + "config.ini") + self.config = config + + def set_stl(self): + """ + Reads in Vehicle surface mesh from STL file. + + Parameters + ---------- + path_to_stl : str + file location for Vehicle's surface mesh using an STL file. + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + """ + path_to_stl = resolve_asset_path(self.case_dir, 'stl', self.config['vv']['stl_lm']) + + self.mesh = mesh.Mesh.from_file(path_to_stl) + self.path_to_stl = path_to_stl + return + + def convert_stl_to_vtk_strikes(self, path_to_vtk, cellData, mesh): + """ + Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. + + Parameters + ---------- + cellData : dict + Dictionary storing arrays that contain all surface properties. + + Returns + ------- + Method doesn't currently return anything. Simply saves data to files as needed. + Does the method need to return a status message? or pass similar data? + """ + + # if self.mesh == None and mesh == None: + # print("mesh is not set. Please load using self.set_stl() method") + # return + + surface = self.mesh if mesh is None else mesh + # Ensure output directory exists and derive base filename + out_dir = self.case_dir + 'results/' + convert_stl_to_vtk(surface, out_dir, filename=path_to_vtk, cellData=cellData) + return + + + def convert_stl_to_vtk(self): + """ + Converts STL mesh to a VTK file and attaches surface properties supplied in cellData. + + Parameters + ---------- + cellData : dict + Dictionary storing arrays that contain all surface properties. + + Returns + ------- + Method doesn't currently return anything. Simply saves data to files as needed. + Does the method need to return a status message? or pass similar data? + """ + + surface = self.mesh + out_dir = self.case_dir + 'results/' + # Default cell data + cellData = {"strikes": np.zeros(len(surface.vectors))} + # Derive filename from path_to_stl if available + filename = None + if hasattr(self, 'path_to_stl') and self.path_to_stl: + filename = self.path_to_stl.split('/')[-1].split('.')[0] + convert_stl_to_vtk(surface, out_dir, filename=filename, cellData=cellData) return \ No newline at end of file diff --git a/pyrpod/vehicle/VisitingVehicle.py b/pyrpod/vehicle/VisitingVehicle.py index f6795b2..2bd99ab 100644 --- a/pyrpod/vehicle/VisitingVehicle.py +++ b/pyrpod/vehicle/VisitingVehicle.py @@ -1,605 +1,605 @@ -import pandas as pd - -from stl import mesh -from mpl_toolkits import mplot3d -from matplotlib import pyplot as plt -import numpy as np -import math -import os - -from pyrpod.vehicle.Vehicle import Vehicle -from pyrpod.mdao import SweepConfig -from pyrpod.logging_utils import get_logger -from pyrpod.util.io.fs import resolve_asset_path - -logger = get_logger("pyrpod.vehicle.VisitingVehicle") - -# Adapted from -# https://stackoverflow.com/questions/54616049/converting-a-rotation-matrix-to-euler-angles-and-back-special-case -def rot2eul(R): - beta = -np.arcsin(R[2][0]) - alpha = np.arctan2(R[2][1]/np.cos(beta),R[2][2]/np.cos(beta)) - gamma = np.arctan2(R[1][0]/np.cos(beta),R[0][0]/np.cos(beta)) - return np.array((alpha, beta, gamma)) - -# Helper functions for constructer. -def process_coordinates(str_coord): - # Split str at spaces - str_list = str_coord.split(' ') - # Return as list of floats - return [float(x) for x in str_list] - -# Process definition of an individual thruster. -def process_thruster_def(str_thruster): - columns = ['name', 'type', 'exit', 'dcm'] - # thruster = pd.DataFrame(columns = columns) - # print(thruster.dtypes) - thruster = {} - # Remove new line char (last char) and split at any space char. - str_list = str_thruster[:-1].split(' ') - # str_list = str_thruster.split(' ') - # print(str_list) - # Save name and type of thruster - thruster["name"] = [str_list.pop(0)] - thruster['type'] = [str_list.pop(0)] - # print(thruster['name']) - # Save coordinate for center of exit plane. - coord = [] - for i in range(3): - coord.append(float(str_list.pop(0))) - thruster['exit'] = [coord] - - # Save direction cosine matrix of thruster relative to the vehicle - drm = [] - for i in range(3): - row = [] - for j in range(3): - row.append(float(str_list.pop(0))) - drm.append(row) - thruster['dcm'] = drm - # thruster = pd.DataFrame(thruster) - # print(thruster) - # return pd.DataFrame(thruster) - return thruster - -# Wrapper function -def process_str_thrusters(str_thrusters): - # dcm = direction cosine matrix - columns = ['name', 'type', 'exit', 'dcm'] - thrusters_data = {} - for thruster in str_thrusters: - name = str(thruster.split(' ')[0]) - thrusters_data[name] = process_thruster_def(thruster) - # print(process_thruster_def(thruster)) - # thrusters_data = pd.concat([thrusters_data,process_thruster_def(thruster)], ignore_index = True) - # print(thrusters_data.dtypes) - - return thrusters_data - -# Process definition of an individual cluster. -def process_cluster_def(str_cluster): - columns = ['name', 'exit', 'dcm'] - cluster = {} - # Remove new line char (last char) and split at any space char. - str_list = str_cluster[:-1].split(' ') - # Save name of cluster - cluster["name"] = [str_list.pop(0)] - # Save coordinate for center of cluster. - coord = [] - for i in range(3): - coord.append(float(str_list.pop(0))) - cluster['exit'] = [coord] - - # Save direction cosine matrix of cluster relative to the vehicle - drm = [] - for i in range(3): - row = [] - for j in range(3): - row.append(float(str_list.pop(0))) - drm.append(row) - cluster['dcm'] = drm - return cluster - -# Wrapper function -def process_str_clusters(str_clusters): - # dcm = direction cosine matrix - columns = ['name', 'exit', 'dcm'] - clusters_data = {} - for cluster in str_clusters: - name = str(cluster.split(' ')[0]) - clusters_data[name] = process_cluster_def(cluster) - - return clusters_data - -class VisitingVehicle(Vehicle): - """ - Class responsible for handling visiting vehicle data. - - Includes surface mesh and thruster configuration data. - - Attributes - ---------- - num_thrusters : int - Total number of thrusters in RCS configuration. - - thruster_units : str - Units for thruster coordinates. - - cog : float - Center of Gravity for the Visiting Vehicle. - - grapple : float - Grappling coordinate for the Visiting Vehicle. - - thruster_data : dictionary - Dictionary holding the main thruster configuration data. - - cluster_data : dictionary - Dictionary holding the main cluster configuration data. - - jet_interactions : float - Can be ignored for now. - - Methods - ------- - set_stl() - Reads in Vehicle surface mesh from STL file. - - set_thruster_config() - Reads the thruster configuration file from the config.ini for the Visiting Vehicle and saves it as class members. - - change_cluster_config() - Alters cluster configuration data using OpenMDAO inputs. - - - set_cluster_config() - Read in cluster configuration data from the provided file path. - - set_thruster_metrics() - Reads the thruster data file to gather thruster-specific performance parameters for the configuration from a .csv file - and saves it in a list of dictionaries. These dictionaries are then saved into each thruster in the configuration. - - print_info() - Simple method to format printing of vehicle info. - - initiate_plume_mesh() - Helper method that reads in surface mesh for plume clone. - - transform_plume_mesh(thruster_id, plumeMesh) - Transform plume mesh according to the specified thruster's DCM and exit coordinate. - - initiate_plume_normal(thruster_id) - Collects plume normal vectors data for visualization. - - plot_vv_and_thruster(plumeMesh, thruster_id, normal, i) - Plots Visiting Vehicle and plume cone for provided thruster id. - - check_thruster_configuration() - Plots visiting vehicle and all thrusters in RCS configuration. - """ - - def print_info(self): - """ - Simple method to format printing of vehicle info. - - Parameters - ---------- - None - - Returns - ------- - None - """ - logger.info('number of thrusters: %s', self.num_thrusters) - logger.info('thruster units: %s', self.thruster_units) - logger.info('center of gravity: %s', self.cog) - logger.info('grapple coordinate: %s', self.grapple) - logger.info('number of dual jet interactions: %s', self.jet_interactions) - return - - def set_stl(self): - """ - Reads in Vehicle surface mesh from STL file. - - Parameters - ---------- - None - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - """ - path_to_stl = resolve_asset_path(self.case_dir, 'stl', self.config['vv']['stl_lm']) - self.mesh = mesh.Mesh.from_file(path_to_stl) - self.path_to_stl = path_to_stl - return - - def get_thruster_cant(self, thruster_name): - """ - Finds the cant angle defined as angle from the LM surface tangent. - Takes the thruster's DCM, undoes the frame transformation - ie. the frame made by the surface tangent and the line from the LM's - axial surface to the exit coordinate, is rotated about x to match the universal YZ axes. - Then the DCM is decomposed to grab the cant angling. - - Parameters - ---------- - thruster_name : string - name of the thruster of interest - - Returns - ------- - float - cant angle in rad - """ - # find frame rotation (Tx) - # taken directly from SweepConfig.SweepDecelAngles.calculate_frame_rot() - exit_coords = self.thruster_data[thruster_name]['exit'][0] - y = exit_coords[1] - z = exit_coords[2] - - if y == 0 and z > 0: - theta = np.pi/2 - elif y == 0 and z < 0: - theta = -np.pi/2 - else: - theta = np.arctan2(z, y) - - Tx = np.array([ - [1, 0, 0], - [0, np.cos(theta), -np.sin(theta)], - [0, np.sin(theta), np.cos(theta)] - ]) - - # Find the inverse of Tx - inv_Tx = np.linalg.inv(Tx) - - # undo the frame rotation - DCM = self.thruster_data[thruster_name]['dcm'] - Rz = np.dot(inv_Tx, DCM) - - # resulting matrix representes the rotation of DCM about z-axis - # Rz -> cant angle - cant = np.arccos(Rz[0][0]) - - return cant - - def set_thruster_config(self, thruster_data=None): - """ - Reads the thruster configuration file from the config.ini for the Visiting Vehicle and saves it as class members. - - If thruster data IS passed, simple overwrite self.thruster_data. - This use is intended to occur only after a notional use of this method. - (ie a method call without thruster_data, using the tcf file path instead.) - - Parameters - ---------- - None - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - """ - if thruster_data is None: - try: - path_to_tcf = resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['tcf']) - except KeyError: - # print("WARNING: Thruster Configuration File not set") - return - # Simple program, reading text from a file. - with open(path_to_tcf, 'r') as f: - lines = f.readlines() - - # Parse through first few lines, save relevant information. - self.num_thrusters = int(lines.pop(0)) - self.thruster_units = lines.pop(0)[0] # dont want '\n' - self.cog = process_coordinates(lines.pop(0)) - self.grapple = process_coordinates(lines.pop(0)) - - # Save all strings containing thruster data in a list - str_thrusters = [] - for i in range(self.num_thrusters): - str_thrusters.append(lines.pop(0)) - - # Parse through strings and save data in a dictionary - self.thruster_data = process_str_thrusters(str_thrusters) - - self.jet_interactions = lines.pop(0) - - else: - self.thruster_data = thruster_data - - self.use_clusters = False - - return - - def change_cluster_config(self, x): - """ - Alters cluster configuration data using OpenMDAO inputs. - - Parameters - ---------- - x : array - Axial position (along the x axis) of the nozzle exit with respect to the LM's docking adapter. - - Returns - ------- - Method doesn't currently return anything. - """ - # print('len(self.cluster_data) is', len(self.cluster_data)) - # print('float(x) is', float(x)) - for cluster in self.cluster_data: - # print('cluster is', cluster) - self.cluster_data[cluster]["exit"][0][0] = float(x) - - def set_cluster_config(self): - """ - Read in cluster configuration data from the provided file path. - Gathers cluster configuration data for the Visiting Vehicle from a .dat file - and saves it as class members. - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - """ - - path_to_ccf = resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['ccf']) - - # Simple program, reading text from a file. - with open(path_to_ccf, 'r') as f: - lines = f.readlines() - - # Parse through first few lines, save relevant information. - self.num_clusters = int(lines.pop(0)) - self.cluster_units = lines.pop(0)[0] # dont want '\n' - - # Save all strings containing cluster data in a list - str_clusters = [] - for i in range(self.num_clusters): - str_clusters.append(lines.pop(0)) - - # Parse through strings and save data in a dictionary - self.cluster_data = process_str_clusters(str_clusters) - - self.use_clusters = True - - return - - def set_thruster_metrics(self): - """ - Reads the csv thruster data file to gather thruster-specific performance parameters for the configuration - and saves it in a list of dictionaries. These dictionaries are then saved into each thruster in the configuration. - - Parameters - ---------- - None - - Returns - ------- - Method doesn't currently return anything. Simply sets class members as needed. - Does the method need to return a status message? or pass similar data? - """ - - # Read in path for thruster metric data. - try: - path_to_thruster_metrics = resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['tdf']) - except KeyError: - # print("WARNING: Thruster Metrics File Not Set") - self.thruster_metrics = None - return - - # specify columns to be read as strings. - str_cols = ['#'] - dict_types = {x: 'str' for x in str_cols} - - # read csv into a pd dataframe - thruster_metrics = pd.read_csv(path_to_thruster_metrics, dtype=dict_types) - # print(thruster_characteristics) - - # convert the dataframe into a list of dictionaries - thruster_metrics_list = thruster_metrics.to_dict(orient='records') - - self.thruster_metrics = {} - - for thruster in thruster_metrics_list: - - # Seperate thruster metrics to form new key value pairs. - thruster_id = thruster['#'] - thruster_metrics = thruster.pop('#') - - # Save thruster metrics - self.thruster_metrics[thruster_id] = thruster - - # print(self.thruster_metrics) - - return - - def initiate_plume_mesh(self): - """ - Helper method that reads in surface mesh for plume clone. - - Parameters - ---------- - None for now. Should/could include cone sizing parameters according to plume physics. - This is easy. Simply produce a "unit cone" ahead of time, and scale the coordinates - using numpy-stl. Cone half-angle can also be pre-programmed. - - Returns - ------- - plumeMesh : mesh.Mesh - Surface mesh constructed from STL file. - """ - # TODO: use STL that is already oriented correctly. - plumeMesh = mesh.Mesh.from_file('../data/stl/mold_funnel.stl') - plumeMesh.translate([0, 0, -50]) - plumeMesh.rotate([1, 0, 0], math.radians(180)) - plumeMesh.points = 0.035 * plumeMesh.points - return plumeMesh - - def transform_plume_mesh(self, thruster_id, plumeMesh): - """ - Transform provided plume mesh according to specified thruster's DCM and exit coordinate. - - Parameters - ---------- - thruster_id : str - String to access thruster via a unique ID. - - plumeMesh : mesh.Mesh - Surface mesh constructed from STL file in initial orientation. - - Returns - ------- - plumeMesh : mesh.Mesh - Surface mesh constructed from STL file in transformed orientation. - - """ - rot = np.array(self.thruster_data[thruster_id]['dcm']) - plumeMesh.rotate_using_matrix(rot.T) - plumeMesh.translate(self.thruster_data[thruster_id]['exit'][0]) - return plumeMesh - - def initiate_plume_normal(self, thruster_id): - """ - Collects plume normal vectors data for visualization. - - Parameters - ---------- - thruster_id : str - String to access thruster via a unique ID. - - Returns - ------- - [X,Y,Z,U,V,W] : 2D List - 2D list contains vector data for plume normal. This is janky but convenient for plotting. - - """ - - X = [] - Y = [] - Z = [] - - U = [] - V = [] - W = [] - - # add position vectors to a list. - position = self.thruster_data[thruster_id]['exit'][0] - X.append(position[0]) - Y.append(position[1]) - Z.append(position[2]) - - - # add normal vectors to a list - dcm = self.thruster_data[thruster_id]['dcm'] - U.append(dcm[0][2]) - V.append(dcm[1][2]) - W.append(dcm[2][2]) - - - return [X,Y,Z,U,V,W] - - def plot_vv_and_thruster(self, plumeMesh, thruster_id, normal, i): - """ - Plots Visiting Vehicle and plume cone for provided thruster id. - - This is useful for a quick sanity check of STL file coordinates. - - Parameters - ---------- - plumeMesh : mesh.Mesh - Surface mesh constructed from STL file in transformed orientation. - - thruster_id : str - String to access thruster via a unique ID. - - normal : 2D List - 2D list contains vector data for plume normal. This is janky but convenient for plotting. - - Returns - ------- - i : int - Integer is passed to the wrapper function for saving images with a sequential naming scheme. - - """ - - # Set up nominal configuration for thruster - VVmesh = self.mesh - - # graph vehicle and vectors. - combined = mesh.Mesh(np.concatenate([VVmesh.data, plumeMesh.data])) - - # Instantiate data str to hold visual plots. - figure = plt.figure() - axes = figure.add_subplot(projection = '3d') - axes.add_collection3d(mplot3d.art3d.Poly3DCollection(VVmesh.vectors)) - - surface = mplot3d.art3d.Poly3DCollection(plumeMesh.vectors) - surface.set_facecolor('orange') - - axes.add_collection3d(surface) - axes.quiver(normal[0], normal[1], normal[2], normal[3], normal[4], normal[5], color = (0,0,0), length=4, normalize=True) - - lim = 7 - axes.set_xlim([-1*lim - 3, lim - 3]) - axes.set_ylim([-1*lim, lim]) - axes.set_zlim([-1*lim, lim]) - - axes.set_xlabel('X') - axes.set_ylabel('Y') - axes.set_zlabel('Z') - - figure.suptitle(self.thruster_data[thruster_id]['name'][0]) - - shift = 0 - - if i < 4: - axes.view_init(azim=0, elev=2*shift) - elif i < 8: - axes.view_init(azim=0, elev=2*shift) - elif i < 12: - axes.view_init(azim=0, elev=2*shift) - else: - axes.view_init(azim=0, elev=2*shift) - - if i < 10: - index = '00' + str(i) - elif i < 100: - index = '0' + str(i) - else: - index = str(i) - # screen_shot = vpl.screenshot_fig() - # vpl.save_fig('img/frame' + str(index) + '.png') - plt.savefig('img/frame' + str(index) + '.png') - return i + 1 - - def check_thruster_configuration(self): - """ - Plots visiting vehicle and all thrusters in RCS configuration. - - Methods loads STL file of VV and turn on all thrusters to check locations + orientations. - - It is useful for a quick sanity check of the RCS configuration. - """ - - # Loop through each thruster, graphing normal vecotr and rotated plume cone. - i = 0 - for thruster_id in self.thruster_data: - - # transform plume mesh to notional position. - plumeMesh = self.initiate_plume_mesh() - - # transform plume mesh according to dcm data of current thruster. - plumeMesh = self.transform_plume_mesh(thruster_id, plumeMesh) - - if not os.path.isdir('stl/tcd/'): - os.system('mkdir stl/tcd') - - plumeMesh.save('stl/tcd/' + str(i) + '.stl') - - normal = self.initiate_plume_normal(thruster_id) - - i = self.plot_vv_and_thruster(plumeMesh, thruster_id, normal, i) - +import pandas as pd + +from stl import mesh +from mpl_toolkits import mplot3d +from matplotlib import pyplot as plt +import numpy as np +import math +import os + +from pyrpod.vehicle.Vehicle import Vehicle +from pyrpod.mdao import SweepConfig +from pyrpod.logging_utils import get_logger +from pyrpod.util.io.fs import resolve_asset_path + +logger = get_logger("pyrpod.vehicle.VisitingVehicle") + +# Adapted from +# https://stackoverflow.com/questions/54616049/converting-a-rotation-matrix-to-euler-angles-and-back-special-case +def rot2eul(R): + beta = -np.arcsin(R[2][0]) + alpha = np.arctan2(R[2][1]/np.cos(beta),R[2][2]/np.cos(beta)) + gamma = np.arctan2(R[1][0]/np.cos(beta),R[0][0]/np.cos(beta)) + return np.array((alpha, beta, gamma)) + +# Helper functions for constructer. +def process_coordinates(str_coord): + # Split str at spaces + str_list = str_coord.split(' ') + # Return as list of floats + return [float(x) for x in str_list] + +# Process definition of an individual thruster. +def process_thruster_def(str_thruster): + columns = ['name', 'type', 'exit', 'dcm'] + # thruster = pd.DataFrame(columns = columns) + # print(thruster.dtypes) + thruster = {} + # Remove new line char (last char) and split at any space char. + str_list = str_thruster[:-1].split(' ') + # str_list = str_thruster.split(' ') + # print(str_list) + # Save name and type of thruster + thruster["name"] = [str_list.pop(0)] + thruster['type'] = [str_list.pop(0)] + # print(thruster['name']) + # Save coordinate for center of exit plane. + coord = [] + for i in range(3): + coord.append(float(str_list.pop(0))) + thruster['exit'] = [coord] + + # Save direction cosine matrix of thruster relative to the vehicle + drm = [] + for i in range(3): + row = [] + for j in range(3): + row.append(float(str_list.pop(0))) + drm.append(row) + thruster['dcm'] = drm + # thruster = pd.DataFrame(thruster) + # print(thruster) + # return pd.DataFrame(thruster) + return thruster + +# Wrapper function +def process_str_thrusters(str_thrusters): + # dcm = direction cosine matrix + columns = ['name', 'type', 'exit', 'dcm'] + thrusters_data = {} + for thruster in str_thrusters: + name = str(thruster.split(' ')[0]) + thrusters_data[name] = process_thruster_def(thruster) + # print(process_thruster_def(thruster)) + # thrusters_data = pd.concat([thrusters_data,process_thruster_def(thruster)], ignore_index = True) + # print(thrusters_data.dtypes) + + return thrusters_data + +# Process definition of an individual cluster. +def process_cluster_def(str_cluster): + columns = ['name', 'exit', 'dcm'] + cluster = {} + # Remove new line char (last char) and split at any space char. + str_list = str_cluster[:-1].split(' ') + # Save name of cluster + cluster["name"] = [str_list.pop(0)] + # Save coordinate for center of cluster. + coord = [] + for i in range(3): + coord.append(float(str_list.pop(0))) + cluster['exit'] = [coord] + + # Save direction cosine matrix of cluster relative to the vehicle + drm = [] + for i in range(3): + row = [] + for j in range(3): + row.append(float(str_list.pop(0))) + drm.append(row) + cluster['dcm'] = drm + return cluster + +# Wrapper function +def process_str_clusters(str_clusters): + # dcm = direction cosine matrix + columns = ['name', 'exit', 'dcm'] + clusters_data = {} + for cluster in str_clusters: + name = str(cluster.split(' ')[0]) + clusters_data[name] = process_cluster_def(cluster) + + return clusters_data + +class VisitingVehicle(Vehicle): + """ + Class responsible for handling visiting vehicle data. + + Includes surface mesh and thruster configuration data. + + Attributes + ---------- + num_thrusters : int + Total number of thrusters in RCS configuration. + + thruster_units : str + Units for thruster coordinates. + + cog : float + Center of Gravity for the Visiting Vehicle. + + grapple : float + Grappling coordinate for the Visiting Vehicle. + + thruster_data : dictionary + Dictionary holding the main thruster configuration data. + + cluster_data : dictionary + Dictionary holding the main cluster configuration data. + + jet_interactions : float + Can be ignored for now. + + Methods + ------- + set_stl() + Reads in Vehicle surface mesh from STL file. + + set_thruster_config() + Reads the thruster configuration file from the config.ini for the Visiting Vehicle and saves it as class members. + + change_cluster_config() + Alters cluster configuration data using OpenMDAO inputs. + + + set_cluster_config() + Read in cluster configuration data from the provided file path. + + set_thruster_metrics() + Reads the thruster data file to gather thruster-specific performance parameters for the configuration from a .csv file + and saves it in a list of dictionaries. These dictionaries are then saved into each thruster in the configuration. + + print_info() + Simple method to format printing of vehicle info. + + initiate_plume_mesh() + Helper method that reads in surface mesh for plume clone. + + transform_plume_mesh(thruster_id, plumeMesh) + Transform plume mesh according to the specified thruster's DCM and exit coordinate. + + initiate_plume_normal(thruster_id) + Collects plume normal vectors data for visualization. + + plot_vv_and_thruster(plumeMesh, thruster_id, normal, i) + Plots Visiting Vehicle and plume cone for provided thruster id. + + check_thruster_configuration() + Plots visiting vehicle and all thrusters in RCS configuration. + """ + + def print_info(self): + """ + Simple method to format printing of vehicle info. + + Parameters + ---------- + None + + Returns + ------- + None + """ + logger.info('number of thrusters: %s', self.num_thrusters) + logger.info('thruster units: %s', self.thruster_units) + logger.info('center of gravity: %s', self.cog) + logger.info('grapple coordinate: %s', self.grapple) + logger.info('number of dual jet interactions: %s', self.jet_interactions) + return + + def set_stl(self): + """ + Reads in Vehicle surface mesh from STL file. + + Parameters + ---------- + None + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + """ + path_to_stl = resolve_asset_path(self.case_dir, 'stl', self.config['vv']['stl_lm']) + self.mesh = mesh.Mesh.from_file(path_to_stl) + self.path_to_stl = path_to_stl + return + + def get_thruster_cant(self, thruster_name): + """ + Finds the cant angle defined as angle from the LM surface tangent. + Takes the thruster's DCM, undoes the frame transformation + ie. the frame made by the surface tangent and the line from the LM's + axial surface to the exit coordinate, is rotated about x to match the universal YZ axes. + Then the DCM is decomposed to grab the cant angling. + + Parameters + ---------- + thruster_name : string + name of the thruster of interest + + Returns + ------- + float + cant angle in rad + """ + # find frame rotation (Tx) + # taken directly from SweepConfig.SweepDecelAngles.calculate_frame_rot() + exit_coords = self.thruster_data[thruster_name]['exit'][0] + y = exit_coords[1] + z = exit_coords[2] + + if y == 0 and z > 0: + theta = np.pi/2 + elif y == 0 and z < 0: + theta = -np.pi/2 + else: + theta = np.arctan2(z, y) + + Tx = np.array([ + [1, 0, 0], + [0, np.cos(theta), -np.sin(theta)], + [0, np.sin(theta), np.cos(theta)] + ]) + + # Find the inverse of Tx + inv_Tx = np.linalg.inv(Tx) + + # undo the frame rotation + DCM = self.thruster_data[thruster_name]['dcm'] + Rz = np.dot(inv_Tx, DCM) + + # resulting matrix representes the rotation of DCM about z-axis + # Rz -> cant angle + cant = np.arccos(Rz[0][0]) + + return cant + + def set_thruster_config(self, thruster_data=None): + """ + Reads the thruster configuration file from the config.ini for the Visiting Vehicle and saves it as class members. + + If thruster data IS passed, simple overwrite self.thruster_data. + This use is intended to occur only after a notional use of this method. + (ie a method call without thruster_data, using the tcf file path instead.) + + Parameters + ---------- + None + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + """ + if thruster_data is None: + try: + path_to_tcf = resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['tcf']) + except KeyError: + # print("WARNING: Thruster Configuration File not set") + return + # Simple program, reading text from a file. + with open(path_to_tcf, 'r') as f: + lines = f.readlines() + + # Parse through first few lines, save relevant information. + self.num_thrusters = int(lines.pop(0)) + self.thruster_units = lines.pop(0)[0] # dont want '\n' + self.cog = process_coordinates(lines.pop(0)) + self.grapple = process_coordinates(lines.pop(0)) + + # Save all strings containing thruster data in a list + str_thrusters = [] + for i in range(self.num_thrusters): + str_thrusters.append(lines.pop(0)) + + # Parse through strings and save data in a dictionary + self.thruster_data = process_str_thrusters(str_thrusters) + + self.jet_interactions = lines.pop(0) + + else: + self.thruster_data = thruster_data + + self.use_clusters = False + + return + + def change_cluster_config(self, x): + """ + Alters cluster configuration data using OpenMDAO inputs. + + Parameters + ---------- + x : array + Axial position (along the x axis) of the nozzle exit with respect to the LM's docking adapter. + + Returns + ------- + Method doesn't currently return anything. + """ + # print('len(self.cluster_data) is', len(self.cluster_data)) + # print('float(x) is', float(x)) + for cluster in self.cluster_data: + # print('cluster is', cluster) + self.cluster_data[cluster]["exit"][0][0] = float(x) + + def set_cluster_config(self): + """ + Read in cluster configuration data from the provided file path. + Gathers cluster configuration data for the Visiting Vehicle from a .dat file + and saves it as class members. + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + """ + + path_to_ccf = resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['ccf']) + + # Simple program, reading text from a file. + with open(path_to_ccf, 'r') as f: + lines = f.readlines() + + # Parse through first few lines, save relevant information. + self.num_clusters = int(lines.pop(0)) + self.cluster_units = lines.pop(0)[0] # dont want '\n' + + # Save all strings containing cluster data in a list + str_clusters = [] + for i in range(self.num_clusters): + str_clusters.append(lines.pop(0)) + + # Parse through strings and save data in a dictionary + self.cluster_data = process_str_clusters(str_clusters) + + self.use_clusters = True + + return + + def set_thruster_metrics(self): + """ + Reads the csv thruster data file to gather thruster-specific performance parameters for the configuration + and saves it in a list of dictionaries. These dictionaries are then saved into each thruster in the configuration. + + Parameters + ---------- + None + + Returns + ------- + Method doesn't currently return anything. Simply sets class members as needed. + Does the method need to return a status message? or pass similar data? + """ + + # Read in path for thruster metric data. + try: + path_to_thruster_metrics = resolve_asset_path(self.case_dir, 'tcd', self.config['tcd']['tdf']) + except KeyError: + # print("WARNING: Thruster Metrics File Not Set") + self.thruster_metrics = None + return + + # specify columns to be read as strings. + str_cols = ['#'] + dict_types = {x: 'str' for x in str_cols} + + # read csv into a pd dataframe + thruster_metrics = pd.read_csv(path_to_thruster_metrics, dtype=dict_types) + # print(thruster_characteristics) + + # convert the dataframe into a list of dictionaries + thruster_metrics_list = thruster_metrics.to_dict(orient='records') + + self.thruster_metrics = {} + + for thruster in thruster_metrics_list: + + # Seperate thruster metrics to form new key value pairs. + thruster_id = thruster['#'] + thruster_metrics = thruster.pop('#') + + # Save thruster metrics + self.thruster_metrics[thruster_id] = thruster + + # print(self.thruster_metrics) + + return + + def initiate_plume_mesh(self): + """ + Helper method that reads in surface mesh for plume clone. + + Parameters + ---------- + None for now. Should/could include cone sizing parameters according to plume physics. + This is easy. Simply produce a "unit cone" ahead of time, and scale the coordinates + using numpy-stl. Cone half-angle can also be pre-programmed. + + Returns + ------- + plumeMesh : mesh.Mesh + Surface mesh constructed from STL file. + """ + # TODO: use STL that is already oriented correctly. + plumeMesh = mesh.Mesh.from_file('../data/stl/mold_funnel.stl') + plumeMesh.translate([0, 0, -50]) + plumeMesh.rotate([1, 0, 0], math.radians(180)) + plumeMesh.points = 0.035 * plumeMesh.points + return plumeMesh + + def transform_plume_mesh(self, thruster_id, plumeMesh): + """ + Transform provided plume mesh according to specified thruster's DCM and exit coordinate. + + Parameters + ---------- + thruster_id : str + String to access thruster via a unique ID. + + plumeMesh : mesh.Mesh + Surface mesh constructed from STL file in initial orientation. + + Returns + ------- + plumeMesh : mesh.Mesh + Surface mesh constructed from STL file in transformed orientation. + + """ + rot = np.array(self.thruster_data[thruster_id]['dcm']) + plumeMesh.rotate_using_matrix(rot.T) + plumeMesh.translate(self.thruster_data[thruster_id]['exit'][0]) + return plumeMesh + + def initiate_plume_normal(self, thruster_id): + """ + Collects plume normal vectors data for visualization. + + Parameters + ---------- + thruster_id : str + String to access thruster via a unique ID. + + Returns + ------- + [X,Y,Z,U,V,W] : 2D List + 2D list contains vector data for plume normal. This is janky but convenient for plotting. + + """ + + X = [] + Y = [] + Z = [] + + U = [] + V = [] + W = [] + + # add position vectors to a list. + position = self.thruster_data[thruster_id]['exit'][0] + X.append(position[0]) + Y.append(position[1]) + Z.append(position[2]) + + + # add normal vectors to a list + dcm = self.thruster_data[thruster_id]['dcm'] + U.append(dcm[0][2]) + V.append(dcm[1][2]) + W.append(dcm[2][2]) + + + return [X,Y,Z,U,V,W] + + def plot_vv_and_thruster(self, plumeMesh, thruster_id, normal, i): + """ + Plots Visiting Vehicle and plume cone for provided thruster id. + + This is useful for a quick sanity check of STL file coordinates. + + Parameters + ---------- + plumeMesh : mesh.Mesh + Surface mesh constructed from STL file in transformed orientation. + + thruster_id : str + String to access thruster via a unique ID. + + normal : 2D List + 2D list contains vector data for plume normal. This is janky but convenient for plotting. + + Returns + ------- + i : int + Integer is passed to the wrapper function for saving images with a sequential naming scheme. + + """ + + # Set up nominal configuration for thruster + VVmesh = self.mesh + + # graph vehicle and vectors. + combined = mesh.Mesh(np.concatenate([VVmesh.data, plumeMesh.data])) + + # Instantiate data str to hold visual plots. + figure = plt.figure() + axes = figure.add_subplot(projection = '3d') + axes.add_collection3d(mplot3d.art3d.Poly3DCollection(VVmesh.vectors)) + + surface = mplot3d.art3d.Poly3DCollection(plumeMesh.vectors) + surface.set_facecolor('orange') + + axes.add_collection3d(surface) + axes.quiver(normal[0], normal[1], normal[2], normal[3], normal[4], normal[5], color = (0,0,0), length=4, normalize=True) + + lim = 7 + axes.set_xlim([-1*lim - 3, lim - 3]) + axes.set_ylim([-1*lim, lim]) + axes.set_zlim([-1*lim, lim]) + + axes.set_xlabel('X') + axes.set_ylabel('Y') + axes.set_zlabel('Z') + + figure.suptitle(self.thruster_data[thruster_id]['name'][0]) + + shift = 0 + + if i < 4: + axes.view_init(azim=0, elev=2*shift) + elif i < 8: + axes.view_init(azim=0, elev=2*shift) + elif i < 12: + axes.view_init(azim=0, elev=2*shift) + else: + axes.view_init(azim=0, elev=2*shift) + + if i < 10: + index = '00' + str(i) + elif i < 100: + index = '0' + str(i) + else: + index = str(i) + # screen_shot = vpl.screenshot_fig() + # vpl.save_fig('img/frame' + str(index) + '.png') + plt.savefig('img/frame' + str(index) + '.png') + return i + 1 + + def check_thruster_configuration(self): + """ + Plots visiting vehicle and all thrusters in RCS configuration. + + Methods loads STL file of VV and turn on all thrusters to check locations + orientations. + + It is useful for a quick sanity check of the RCS configuration. + """ + + # Loop through each thruster, graphing normal vecotr and rotated plume cone. + i = 0 + for thruster_id in self.thruster_data: + + # transform plume mesh to notional position. + plumeMesh = self.initiate_plume_mesh() + + # transform plume mesh according to dcm data of current thruster. + plumeMesh = self.transform_plume_mesh(thruster_id, plumeMesh) + + if not os.path.isdir('stl/tcd/'): + os.system('mkdir stl/tcd') + + plumeMesh.save('stl/tcd/' + str(i) + '.stl') + + normal = self.initiate_plume_normal(thruster_id) + + i = self.plot_vv_and_thruster(plumeMesh, thruster_id, normal, i) + return \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 7ed1462..b0a9a80 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,8 @@ -[flake8] -max-line-length = 88 -extend-ignore = E203, W503 -exclude = .git,__pycache__,venv*,build,dist,venv-pyrpod -max-complexity = 12 -# Keep plugin-aware selects looser; add plugins if you use them -select = C,E,F,W,B -application-import-names = pyrpod +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 +exclude = .git,__pycache__,venv*,build,dist,venv-pyrpod +max-complexity = 12 +# Keep plugin-aware selects looser; add plugins if you use them +select = C,E,F,W,B +application-import-names = pyrpod diff --git a/tests/mdao/mdao_integration_test_01.py b/tests/mdao/mdao_integration_test_01.py index f0b8dfe..ac173b7 100644 --- a/tests/mdao/mdao_integration_test_01.py +++ b/tests/mdao/mdao_integration_test_01.py @@ -1,21 +1,21 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/mdao/mdao_integration_test_01.py -# ======================== -# Write test case description. - -import unittest - -class MDAOTest(unittest.TestCase): - def test_mdao(self): - - # print("mdao integration test") - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/mdao/mdao_integration_test_01.py +# ======================== +# Write test case description. + +import unittest + +class MDAOTest(unittest.TestCase): + def test_mdao(self): + + # print("mdao integration test") + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mdao/mdao_unit_test_01.py b/tests/mdao/mdao_unit_test_01.py index 5647c46..a4c8fa8 100644 --- a/tests/mdao/mdao_unit_test_01.py +++ b/tests/mdao/mdao_unit_test_01.py @@ -1,21 +1,21 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/mdao/mdao_unit_test_01.py -# ======================== -# Write test case description. - -import unittest - -class MDAOTest(unittest.TestCase): - def test_mdao(self): - - # print("mdao unit test") - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/mdao/mdao_unit_test_01.py +# ======================== +# Write test case description. + +import unittest + +class MDAOTest(unittest.TestCase): + def test_mdao(self): + + # print("mdao unit test") + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mdao/mdao_unit_test_02.py b/tests/mdao/mdao_unit_test_02.py index 1c5b4ef..550d847 100644 --- a/tests/mdao/mdao_unit_test_02.py +++ b/tests/mdao/mdao_unit_test_02.py @@ -1,100 +1,100 @@ -# Juan P. Roldan -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-28-24 - -# ======================== -# PyRPOD: test/mdao/mdao_unit_test_03.py -# ======================== -# A test case to create array of cant angle swept thruster configurations. -# The sweep assumes the given thrusters angle symmetrically. -# This means that opposite pitch thrusters angle opposite but equally to each other -# and yaw thrusters angle opposite but equally to each other -# !!currently, the pitch and yaw thrusters are canted in unison!! -# pitch thrusters are not angled such that they can produce a yaw -# yaw thrusters are not angled such that they can introduce a pitch - -# TODO: Re-factor code to save data in a relevant object. Also add files to save to. - -import unittest, os, sys -import numpy as np - -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.vehicle import TargetVehicle, VisitingVehicle -from pyrpod.mdao import SweepConfig - - -class CoordinateSweepCheck(unittest.TestCase): - def test_cant_sweep(self): - - # # number of deceleration thrusters to evenly distribute - # # !!!IMPORTANT!!! also ensure to update jfh, fire all thrusters one wants to visualize!!! - # nthrusters = 7 - - # # define the LM's radius - # r = 2 # m - - # # creating an example configuration - - # # identity matrix as the default for all thrusters - # # techically not needed, since all the thrusters are set to -x group - # # these thrusters DCMs are standardized within SweepConfig - # dcm = np.eye(3) - - # # creating example thruster groups, to confirm thrusters that are for decel - # thruster_groups = { - # '+x': [], - # 'neg_x': [], - # '+y': [], - # '-y': [], - # '+z': [], - # '-z': [], - # '+pitch': [], - # '-pitch': [], - # '+yaw' : [], - # '-yaw' : [] - # } - - # config = {} - - # # name each thruster, as corresponding to different packs - # # evenly distribute the exit coordinate of each thruster - # # append each thruster to deceleration group - # for i in range(1, nthrusters+1): - # name = 'P' + str(i) + 'T1' - # exit = [0, r*np.cos((i-1)*(2 * np.pi / nthrusters)), r*np.sin((i-1)*(2 * np.pi / nthrusters))] - # config[name] = {'name': [name], 'type': ['001'], 'exit': [exit], 'dcm': dcm} - # thruster_groups['neg_x'].append(name) - - # # define step sizes for each angle - # dcant = 10 # deg - - # # create SweepAngles object - # angle_sweep = SweepConfig.SweepDecelAngles(config, thruster_groups) - - # # call sweep_long_thruster on the configuration and print the DCM's - # config_swept_array = angle_sweep.sweep_decel_thrusters_all(dcant) - - # for i, config in enumerate(config_swept_array): - # # Path to directory holding data assets and results for a specific RPOD study. - # case_dir = '../case/mdao/cant_sweep/' - - # # Load JFH data. - # jfh = JetFiringHistory.JetFiringHistory(case_dir) - # jfh.read_jfh() - - # tv = TargetVehicle.TargetVehicle(case_dir) - # tv.set_stl() - - # vv = VisitingVehicle.VisitingVehicle(case_dir) - # vv.set_stl() - # vv.set_thruster_config(config) - - # rpod = RPOD.RPOD(case_dir) - # rpod.study_init(jfh, tv, vv) - - # rpod.visualize_sweep(i) - return - -if __name__ == '__main__': +# Juan P. Roldan +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-28-24 + +# ======================== +# PyRPOD: test/mdao/mdao_unit_test_03.py +# ======================== +# A test case to create array of cant angle swept thruster configurations. +# The sweep assumes the given thrusters angle symmetrically. +# This means that opposite pitch thrusters angle opposite but equally to each other +# and yaw thrusters angle opposite but equally to each other +# !!currently, the pitch and yaw thrusters are canted in unison!! +# pitch thrusters are not angled such that they can produce a yaw +# yaw thrusters are not angled such that they can introduce a pitch + +# TODO: Re-factor code to save data in a relevant object. Also add files to save to. + +import unittest, os, sys +import numpy as np + +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.vehicle import TargetVehicle, VisitingVehicle +from pyrpod.mdao import SweepConfig + + +class CoordinateSweepCheck(unittest.TestCase): + def test_cant_sweep(self): + + # # number of deceleration thrusters to evenly distribute + # # !!!IMPORTANT!!! also ensure to update jfh, fire all thrusters one wants to visualize!!! + # nthrusters = 7 + + # # define the LM's radius + # r = 2 # m + + # # creating an example configuration + + # # identity matrix as the default for all thrusters + # # techically not needed, since all the thrusters are set to -x group + # # these thrusters DCMs are standardized within SweepConfig + # dcm = np.eye(3) + + # # creating example thruster groups, to confirm thrusters that are for decel + # thruster_groups = { + # '+x': [], + # 'neg_x': [], + # '+y': [], + # '-y': [], + # '+z': [], + # '-z': [], + # '+pitch': [], + # '-pitch': [], + # '+yaw' : [], + # '-yaw' : [] + # } + + # config = {} + + # # name each thruster, as corresponding to different packs + # # evenly distribute the exit coordinate of each thruster + # # append each thruster to deceleration group + # for i in range(1, nthrusters+1): + # name = 'P' + str(i) + 'T1' + # exit = [0, r*np.cos((i-1)*(2 * np.pi / nthrusters)), r*np.sin((i-1)*(2 * np.pi / nthrusters))] + # config[name] = {'name': [name], 'type': ['001'], 'exit': [exit], 'dcm': dcm} + # thruster_groups['neg_x'].append(name) + + # # define step sizes for each angle + # dcant = 10 # deg + + # # create SweepAngles object + # angle_sweep = SweepConfig.SweepDecelAngles(config, thruster_groups) + + # # call sweep_long_thruster on the configuration and print the DCM's + # config_swept_array = angle_sweep.sweep_decel_thrusters_all(dcant) + + # for i, config in enumerate(config_swept_array): + # # Path to directory holding data assets and results for a specific RPOD study. + # case_dir = '../case/mdao/cant_sweep/' + + # # Load JFH data. + # jfh = JetFiringHistory.JetFiringHistory(case_dir) + # jfh.read_jfh() + + # tv = TargetVehicle.TargetVehicle(case_dir) + # tv.set_stl() + + # vv = VisitingVehicle.VisitingVehicle(case_dir) + # vv.set_stl() + # vv.set_thruster_config(config) + + # rpod = RPOD.RPOD(case_dir) + # rpod.study_init(jfh, tv, vv) + + # rpod.visualize_sweep(i) + return + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mdao/mdao_verification_test_04.py b/tests/mdao/mdao_verification_test_04.py index 55d94ed..2db0dc7 100644 --- a/tests/mdao/mdao_verification_test_04.py +++ b/tests/mdao/mdao_verification_test_04.py @@ -1,76 +1,76 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/mdao/mdao_verification_test_01.py -# ======================== -# Initial variable sweep study used to explore the maximum overshoot velocity an LM can handle -# if the thruster configuration and decelaration starting distance are held constant. - -import unittest - -from pyrpod.vehicle import TargetVehicle, LogisticsModule -from pyrpod.mdao import TradeStudy - -class MDAOTest(unittest.TestCase): - def test_mdao(self): - - # # 1. Set Up - # # Load in Fixed LM and thruster configuation for trade study. - # case_dir = '../case/mdao/trade_study/' - - # # Load Target Vehicle. - # tv = TargetVehicle.TargetVehicle(case_dir) - # tv.set_stl() - - # # Instantiate LogisticModule object. - # lm = LogisticsModule.LogisticsModule(case_dir) - - # # Define LM mass distribution properties. - # m = 14000 # kg - # h = 11 # m - # r = 2 # m - # lm.set_inertial_props(m, h, r) - - # # Load in thruster configuration. - # lm.set_thruster_config() - # lm.set_thruster_metrics() - # lm.assign_thruster_groups() - - # # Define LM Docking conditions - # v_ida = 0.1 # m/s (target velocity for safe docking) - # tv.set_v_ida(v_ida) - # r_o = 20 # m (initial distance at start of initial burn) - # tv.set_r_o(r_o) - - # # Determine design variables to vary over. - # # axial_overshoot = [0, 25, 50, 75, 100] # m/s (WIP, replace with physical values) - # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) - # axial_thruster_pos = [0, 2.75, 5.5, 8.25, 11] # m (ignoring solar panel since decel thrusters) - # surface_cant_angle = [0, 15, 30, 45, 60] # degrees - - # sweep_vars = { - # 'axial_overshoot': axial_overshoot, - # # 'axial_thruster_pos': axial_thruster_pos, - # # 'surface_cant_angle': surface_cant_angle - # } - - # # 2. Excecute - # # Produce data for trade study by running docking analysis according to relevant design variable sweeps. - # study = TradeStudy.TradeStudy(case_dir) - # results = study.run_axial_overshoot_sweep(sweep_vars, lm, tv) - - # # Post process results and perform trade studies analysis. - # # design_metrics = ['fuel_usage', 'plume', 'maneuver', 'safety'] - # # ideal_configs = study.process_results(design_metrics, results) - - # # 3. Assert - # # TBD. This will be developed at the very end to lock in desired results given - - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/mdao/mdao_verification_test_01.py +# ======================== +# Initial variable sweep study used to explore the maximum overshoot velocity an LM can handle +# if the thruster configuration and decelaration starting distance are held constant. + +import unittest + +from pyrpod.vehicle import TargetVehicle, LogisticsModule +from pyrpod.mdao import TradeStudy + +class MDAOTest(unittest.TestCase): + def test_mdao(self): + + # # 1. Set Up + # # Load in Fixed LM and thruster configuation for trade study. + # case_dir = '../case/mdao/trade_study/' + + # # Load Target Vehicle. + # tv = TargetVehicle.TargetVehicle(case_dir) + # tv.set_stl() + + # # Instantiate LogisticModule object. + # lm = LogisticsModule.LogisticsModule(case_dir) + + # # Define LM mass distribution properties. + # m = 14000 # kg + # h = 11 # m + # r = 2 # m + # lm.set_inertial_props(m, h, r) + + # # Load in thruster configuration. + # lm.set_thruster_config() + # lm.set_thruster_metrics() + # lm.assign_thruster_groups() + + # # Define LM Docking conditions + # v_ida = 0.1 # m/s (target velocity for safe docking) + # tv.set_v_ida(v_ida) + # r_o = 20 # m (initial distance at start of initial burn) + # tv.set_r_o(r_o) + + # # Determine design variables to vary over. + # # axial_overshoot = [0, 25, 50, 75, 100] # m/s (WIP, replace with physical values) + # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) + # axial_thruster_pos = [0, 2.75, 5.5, 8.25, 11] # m (ignoring solar panel since decel thrusters) + # surface_cant_angle = [0, 15, 30, 45, 60] # degrees + + # sweep_vars = { + # 'axial_overshoot': axial_overshoot, + # # 'axial_thruster_pos': axial_thruster_pos, + # # 'surface_cant_angle': surface_cant_angle + # } + + # # 2. Excecute + # # Produce data for trade study by running docking analysis according to relevant design variable sweeps. + # study = TradeStudy.TradeStudy(case_dir) + # results = study.run_axial_overshoot_sweep(sweep_vars, lm, tv) + + # # Post process results and perform trade studies analysis. + # # design_metrics = ['fuel_usage', 'plume', 'maneuver', 'safety'] + # # ideal_configs = study.process_results(design_metrics, results) + + # # 3. Assert + # # TBD. This will be developed at the very end to lock in desired results given + + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mdao/mdao_verification_test_05.py b/tests/mdao/mdao_verification_test_05.py index ebb8db1..0bdc62a 100644 --- a/tests/mdao/mdao_verification_test_05.py +++ b/tests/mdao/mdao_verification_test_05.py @@ -1,75 +1,75 @@ -# Juan P. Roldan -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 04-04-24 - - -# ======================== -# PyRPOD: test/mdao/mdao_verification_test_02.py -# ======================== -# - -import unittest - -from pyrpod.vehicle import TargetVehicle, LogisticsModule -from pyrpod.mdao import TradeStudy - -class MDAOTest(unittest.TestCase): - def test_mdao(self): - - # # 1. Set Up - # # Load in Fixed LM and thruster configuation for trade study. - # case_dir = '../case/mdao/trade_study/' - - # # Load Target Vehicle. - # tv = TargetVehicle.TargetVehicle(case_dir) - # tv.set_stl() - - # # Instantiate LogisticModule object. - # lm = LogisticsModule.LogisticsModule(case_dir) - - # # Define LM mass distribution properties. - # m = 14000 # kg - # h = 11 # m - # r = 2 # m - # lm.set_inertial_props(m, h, r) - - # # Load in thruster configuration. - # lm.set_thruster_config() - # lm.set_thruster_metrics() - # lm.assign_thruster_groups() - - # # Define LM Docking conditions - # v_ida = 0.03 # m/s (target velocity for safe docking) - # tv.set_v_ida(v_ida) - # r_o = 20 # m (initial distance at start of initial burn) - # tv.set_r_o(r_o) - - # # Determine design variables to vary over. - # # axial_overshoot = [0, 25, 50, 75, 100] # m/s (WIP, replace with physical values) - # # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) - # # axial_thruster_pos = [0, 2.75, 5.5, 8.25, 11] # m (ignoring solar panel since decel thrusters) - # surface_cant_angles = [0, 15, 30, 45, 60] # degrees - - # sweep_vars = { - # 'axial_overshoot': 1, - # # 'axial_thruster_pos': axial_thruster_pos, - # 'surface_cant_angles': surface_cant_angles - # } - - # # 2. Excecute - # # Produce data for trade study by running docking analysis according to relevant design variable sweeps. - # study = TradeStudy.TradeStudy(case_dir) - # results = study.run_surface_cant_sweep(sweep_vars, lm, tv) - - # # Post process results and perform trade studies analysis. - # # design_metrics = ['fuel_usage', 'plume', 'maneuver', 'safety'] - # # ideal_configs = study.process_results(design_metrics, results) - - # # 3. Assert - # # TBD. This will be developed at the very end to lock in desired results given - - return - -if __name__ == '__main__': - unittest.main() +# Juan P. Roldan +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 04-04-24 + + +# ======================== +# PyRPOD: test/mdao/mdao_verification_test_02.py +# ======================== +# + +import unittest + +from pyrpod.vehicle import TargetVehicle, LogisticsModule +from pyrpod.mdao import TradeStudy + +class MDAOTest(unittest.TestCase): + def test_mdao(self): + + # # 1. Set Up + # # Load in Fixed LM and thruster configuation for trade study. + # case_dir = '../case/mdao/trade_study/' + + # # Load Target Vehicle. + # tv = TargetVehicle.TargetVehicle(case_dir) + # tv.set_stl() + + # # Instantiate LogisticModule object. + # lm = LogisticsModule.LogisticsModule(case_dir) + + # # Define LM mass distribution properties. + # m = 14000 # kg + # h = 11 # m + # r = 2 # m + # lm.set_inertial_props(m, h, r) + + # # Load in thruster configuration. + # lm.set_thruster_config() + # lm.set_thruster_metrics() + # lm.assign_thruster_groups() + + # # Define LM Docking conditions + # v_ida = 0.03 # m/s (target velocity for safe docking) + # tv.set_v_ida(v_ida) + # r_o = 20 # m (initial distance at start of initial burn) + # tv.set_r_o(r_o) + + # # Determine design variables to vary over. + # # axial_overshoot = [0, 25, 50, 75, 100] # m/s (WIP, replace with physical values) + # # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) + # # axial_thruster_pos = [0, 2.75, 5.5, 8.25, 11] # m (ignoring solar panel since decel thrusters) + # surface_cant_angles = [0, 15, 30, 45, 60] # degrees + + # sweep_vars = { + # 'axial_overshoot': 1, + # # 'axial_thruster_pos': axial_thruster_pos, + # 'surface_cant_angles': surface_cant_angles + # } + + # # 2. Excecute + # # Produce data for trade study by running docking analysis according to relevant design variable sweeps. + # study = TradeStudy.TradeStudy(case_dir) + # results = study.run_surface_cant_sweep(sweep_vars, lm, tv) + + # # Post process results and perform trade studies analysis. + # # design_metrics = ['fuel_usage', 'plume', 'maneuver', 'safety'] + # # ideal_configs = study.process_results(design_metrics, results) + + # # 3. Assert + # # TBD. This will be developed at the very end to lock in desired results given + + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mdao/mdao_verification_test_06.py b/tests/mdao/mdao_verification_test_06.py index 0eeae5e..8952d01 100644 --- a/tests/mdao/mdao_verification_test_06.py +++ b/tests/mdao/mdao_verification_test_06.py @@ -1,77 +1,77 @@ -# Juan P. Roldan -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 04-12-24 - - -# ======================== -# PyRPOD: test/mdao/mdao_verification_test_03.py -# ======================== -# - -import unittest - -from pyrpod.vehicle import TargetVehicle, LogisticsModule -from pyrpod.mdao import TradeStudy - -class MDAOTest(unittest.TestCase): - def test_mdao(self): - - # # 1. Set Up - # # Load in Fixed LM and thruster configuation for trade study. - # case_dir = '../case/mdao/trade_study/' - - # # Load Target Vehicle. - # tv = TargetVehicle.TargetVehicle(case_dir) - # tv.set_stl() - - # # Instantiate LogisticModule object. - # lm = LogisticsModule.LogisticsModule(case_dir) - - # # Define LM mass distribution properties. - # m = 14000 # kg - # h = 8.5 # m - # r = 1.65 # m - # lm.set_inertial_props(m, h, r) - - # # Load in thruster configuration. - # lm.set_thruster_config() - # lm.set_thruster_metrics() - # lm.assign_thruster_groups() - - # # Define LM Docking conditions - # v_ida = 0.03 # m/s (target velocity for safe docking) - # tv.set_v_ida(v_ida) - # r_o = 20 # m (initial distance at start of initial burn) - # tv.set_r_o(r_o) - - # # Determine design variables to vary over. - # # axial_overshoot = [0, 25, 50, 75, 100] # m/s (WIP, replace with physical values) - # # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) - # # axial_thruster_pos = [0, 2.75, 5.5, 8.25, 11] # m (ignoring solar panel since decel thrusters) - # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) - # surface_cant_angles = [0, 15, 30, 45, 60] # degrees - - # sweep_vars = { - # 'axial_overshoot': axial_overshoot, - # # 'axial_thruster_pos': axial_thruster_pos, - # 'surface_cant_angles': surface_cant_angles - # } - - # # 2. Excecute - # # Produce data for trade study by running docking analysis according to relevant design variable sweeps. - # study = TradeStudy.TradeStudy(case_dir) - # results = study.run_multi_var_sweep(sweep_vars, lm, tv) - - # # Post process results and perform trade studies analysis. - # # design_metrics = ['fuel_usage', 'plume', 'maneuver', 'safety'] - # # ideal_configs = study.process_results(design_metrics, results) - - # # 3. Assert - # # TBD. This will be developed at the very end to lock in desired results given - - return - -if __name__ == '__main__': - unittest.main() - +# Juan P. Roldan +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 04-12-24 + + +# ======================== +# PyRPOD: test/mdao/mdao_verification_test_03.py +# ======================== +# + +import unittest + +from pyrpod.vehicle import TargetVehicle, LogisticsModule +from pyrpod.mdao import TradeStudy + +class MDAOTest(unittest.TestCase): + def test_mdao(self): + + # # 1. Set Up + # # Load in Fixed LM and thruster configuation for trade study. + # case_dir = '../case/mdao/trade_study/' + + # # Load Target Vehicle. + # tv = TargetVehicle.TargetVehicle(case_dir) + # tv.set_stl() + + # # Instantiate LogisticModule object. + # lm = LogisticsModule.LogisticsModule(case_dir) + + # # Define LM mass distribution properties. + # m = 14000 # kg + # h = 8.5 # m + # r = 1.65 # m + # lm.set_inertial_props(m, h, r) + + # # Load in thruster configuration. + # lm.set_thruster_config() + # lm.set_thruster_metrics() + # lm.assign_thruster_groups() + + # # Define LM Docking conditions + # v_ida = 0.03 # m/s (target velocity for safe docking) + # tv.set_v_ida(v_ida) + # r_o = 20 # m (initial distance at start of initial burn) + # tv.set_r_o(r_o) + + # # Determine design variables to vary over. + # # axial_overshoot = [0, 25, 50, 75, 100] # m/s (WIP, replace with physical values) + # # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) + # # axial_thruster_pos = [0, 2.75, 5.5, 8.25, 11] # m (ignoring solar panel since decel thrusters) + # axial_overshoot = lm.calc_overshoot_v_range(v_ida, r_o) + # surface_cant_angles = [0, 15, 30, 45, 60] # degrees + + # sweep_vars = { + # 'axial_overshoot': axial_overshoot, + # # 'axial_thruster_pos': axial_thruster_pos, + # 'surface_cant_angles': surface_cant_angles + # } + + # # 2. Excecute + # # Produce data for trade study by running docking analysis according to relevant design variable sweeps. + # study = TradeStudy.TradeStudy(case_dir) + # results = study.run_multi_var_sweep(sweep_vars, lm, tv) + + # # Post process results and perform trade studies analysis. + # # design_metrics = ['fuel_usage', 'plume', 'maneuver', 'safety'] + # # ideal_configs = study.process_results(design_metrics, results) + + # # 3. Assert + # # TBD. This will be developed at the very end to lock in desired results given + + return + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/mission/mission_integration_test_01.py b/tests/mission/mission_integration_test_01.py index 3d688fc..67e22dc 100644 --- a/tests/mission/mission_integration_test_01.py +++ b/tests/mission/mission_integration_test_01.py @@ -1,115 +1,115 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_01.py -# ======================== -# A brief test case to calculate the 6DOF performance of each individual thruster in the LM - -import unittest, os, sys -import numpy as np - -from pyrpod.vehicle import LogisticsModule - -def expected_normal_vector(index): - # Define normal vectors for each thruster based on the repeating pattern in test_output.txt - normal_vectors = [ - [1.0, 0.0, 0.0], - [-1.0, -0.0, -0.0], - [0.0, -0.7071, 0.7071], - [0.0, 0.7071, -0.7071] - ] - return normal_vectors[index % 4] - -def expected_force_vector(index): - # Define force vectors for each thruster - force_vectors = [ - [-400.0, -0.0, -0.0], - [400.0, 0.0, 0.0], - [0.0, 282.84, -282.84], - [0.0, -282.84, 282.84] - ] - # Adjust for each P1-P8 group if there's any pattern; otherwise, use modulo 4 - return force_vectors[index % 4] - -def expected_translational_acceleration(index): - # Define translational acceleration for each thruster - translational_accelerations = [ - [-0.03, -0.0, -0.0], - [0.03, 0.0, 0.0], - [0.0, 0.021, -0.021], - [0.0, -0.021, 0.021] - ] - return translational_accelerations[index % 4] - -def expected_torque(index): - # Define torques for each thruster - torques = [ - [0.0, -0.02199852, -0.02199852], - [0.0, 0.02199852, 0.02199852], - [0.0, 0.01047556, -0.01047556], - [0.0, -0.01047556, 0.01047556] - ] - if index >= 16 and index < 20: - # Special case for thrusters P5T3, P5T4, etc. with torque values adjusted - if index % 4 == 2: - return [0.0, 0.06285333, -0.06285333] - elif index % 4 == 3: - return [0.0, -0.06285333, 0.06285333] - elif index >= 20 and index < 24: - # Special case for thrusters P6T3, P6T4, etc. - if index % 4 == 2: - return [0.0, -0.06285333, -0.06285333] - elif index % 4 == 3: - return [0.0, 0.06285333, 0.06285333] - elif index >= 28 and index < 32: - # Special case for thrusters P7T3, P7T4, etc. - if index % 4 == 2: - return [0.0, -0.06285333, 0.06285333] - elif index % 4 == 3: - return [0.0, 0.06285333, -0.06285333] - - return torques[index % 4] - - -class IndividualThrusterChecks(unittest.TestCase): - def test_performance_per_thruster(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration data from text file - lm.set_thruster_config() - - # Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - test_output = lm.calc_thruster_performance() - - # print(test_output) - - # Assert results versus expected values. - for index, thruster_data in enumerate(test_output): - # assert thruster_data['thruster_id'] == f"P{(index // 4) + 1}T{(index % 4) + 1}", f"Thruster ID mismatch at index {index}" - # assert thruster_data['normal_vector'] == expected_normal_vector(index), f"Normal vector mismatch at index {index}" - # assert (thruster_data['force_vector'] == expected_force_vector(index)).all(), f"Force vector mismatch at index {index}" - # assert (thruster_data['translational_acceleration'] == expected_translational_acceleration(index)).all(), f"Translational acceleration mismatch at index {index}" - # assert (thruster_data['torque'] == expected_torque(index)).all(), f"Torque mismatch at index {index}" - - actual_torque = thruster_data['torque'] - expected = expected_torque(index) - # assert np.allclose(actual_torque, expected, atol=1e-6), f"Torque mismatch at index {index}: {actual_torque} != {expected}" - - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_01.py +# ======================== +# A brief test case to calculate the 6DOF performance of each individual thruster in the LM + +import unittest, os, sys +import numpy as np + +from pyrpod.vehicle import LogisticsModule + +def expected_normal_vector(index): + # Define normal vectors for each thruster based on the repeating pattern in test_output.txt + normal_vectors = [ + [1.0, 0.0, 0.0], + [-1.0, -0.0, -0.0], + [0.0, -0.7071, 0.7071], + [0.0, 0.7071, -0.7071] + ] + return normal_vectors[index % 4] + +def expected_force_vector(index): + # Define force vectors for each thruster + force_vectors = [ + [-400.0, -0.0, -0.0], + [400.0, 0.0, 0.0], + [0.0, 282.84, -282.84], + [0.0, -282.84, 282.84] + ] + # Adjust for each P1-P8 group if there's any pattern; otherwise, use modulo 4 + return force_vectors[index % 4] + +def expected_translational_acceleration(index): + # Define translational acceleration for each thruster + translational_accelerations = [ + [-0.03, -0.0, -0.0], + [0.03, 0.0, 0.0], + [0.0, 0.021, -0.021], + [0.0, -0.021, 0.021] + ] + return translational_accelerations[index % 4] + +def expected_torque(index): + # Define torques for each thruster + torques = [ + [0.0, -0.02199852, -0.02199852], + [0.0, 0.02199852, 0.02199852], + [0.0, 0.01047556, -0.01047556], + [0.0, -0.01047556, 0.01047556] + ] + if index >= 16 and index < 20: + # Special case for thrusters P5T3, P5T4, etc. with torque values adjusted + if index % 4 == 2: + return [0.0, 0.06285333, -0.06285333] + elif index % 4 == 3: + return [0.0, -0.06285333, 0.06285333] + elif index >= 20 and index < 24: + # Special case for thrusters P6T3, P6T4, etc. + if index % 4 == 2: + return [0.0, -0.06285333, -0.06285333] + elif index % 4 == 3: + return [0.0, 0.06285333, 0.06285333] + elif index >= 28 and index < 32: + # Special case for thrusters P7T3, P7T4, etc. + if index % 4 == 2: + return [0.0, -0.06285333, 0.06285333] + elif index % 4 == 3: + return [0.0, 0.06285333, -0.06285333] + + return torques[index % 4] + + +class IndividualThrusterChecks(unittest.TestCase): + def test_performance_per_thruster(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration data from text file + lm.set_thruster_config() + + # Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + test_output = lm.calc_thruster_performance() + + # print(test_output) + + # Assert results versus expected values. + for index, thruster_data in enumerate(test_output): + # assert thruster_data['thruster_id'] == f"P{(index // 4) + 1}T{(index % 4) + 1}", f"Thruster ID mismatch at index {index}" + # assert thruster_data['normal_vector'] == expected_normal_vector(index), f"Normal vector mismatch at index {index}" + # assert (thruster_data['force_vector'] == expected_force_vector(index)).all(), f"Force vector mismatch at index {index}" + # assert (thruster_data['translational_acceleration'] == expected_translational_acceleration(index)).all(), f"Translational acceleration mismatch at index {index}" + # assert (thruster_data['torque'] == expected_torque(index)).all(), f"Torque mismatch at index {index}" + + actual_torque = thruster_data['torque'] + expected = expected_torque(index) + # assert np.allclose(actual_torque, expected, atol=1e-6), f"Torque mismatch at index {index}: {actual_torque} != {expected}" + + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_02.py b/tests/mission/mission_integration_test_02.py index 09530e1..c05a9d9 100644 --- a/tests/mission/mission_integration_test_02.py +++ b/tests/mission/mission_integration_test_02.py @@ -1,38 +1,38 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_02.py -# ======================== -# A brief test case to calculate the 6DOF performance of thruster working groups - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule - -class ThrusterGroupingChecks(unittest.TestCase): - def test_performance_per_thruster_group(self): - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - - # # Instantiate LogisticModule object. - # lm = LogisticsModule.LogisticsModule(m, h, r) - - # # Load in thruster configuration data from text file - # lm.add_thruster_config('../data/tcd/tcf_flight_envelopes.txt') - - # # Draco/Hypergolic thrusters - # lm.add_thruster_performance(400, 300) - - # # Assign thruster groups and calculate their performance. - # # (creates unwanted files of data so test is not run for now.) - # lm.assign_thruster_groups() - # lm.check_thruster_groups() - - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_02.py +# ======================== +# A brief test case to calculate the 6DOF performance of thruster working groups + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule + +class ThrusterGroupingChecks(unittest.TestCase): + def test_performance_per_thruster_group(self): + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + + # # Instantiate LogisticModule object. + # lm = LogisticsModule.LogisticsModule(m, h, r) + + # # Load in thruster configuration data from text file + # lm.add_thruster_config('../data/tcd/tcf_flight_envelopes.txt') + + # # Draco/Hypergolic thrusters + # lm.add_thruster_performance(400, 300) + + # # Assign thruster groups and calculate their performance. + # # (creates unwanted files of data so test is not run for now.) + # lm.assign_thruster_groups() + # lm.check_thruster_groups() + + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_03.py b/tests/mission/mission_integration_test_03.py index fa93ab9..76f0c85 100644 --- a/tests/mission/mission_integration_test_03.py +++ b/tests/mission/mission_integration_test_03.py @@ -1,45 +1,45 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_03.py -# ======================== -# A brief test case to calculate RCS perfomance for a given flight plan approximating Δv requirements. - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class FlightPlanChecks(unittest.TestCase): - def test_rcs_flight_performance(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration data from text file - lm.set_thruster_config() - - # Assign properties of Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - lm.assign_thruster_groups() - - # Calculate simple 1D flight performance - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - mp.set_lm(lm) - mp.flight_eval.read_flight_plan(lm) - mp.flight_eval.calc_flight_performance() - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_03.py +# ======================== +# A brief test case to calculate RCS perfomance for a given flight plan approximating Δv requirements. + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class FlightPlanChecks(unittest.TestCase): + def test_rcs_flight_performance(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration data from text file + lm.set_thruster_config() + + # Assign properties of Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + lm.assign_thruster_groups() + + # Calculate simple 1D flight performance + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + mp.set_lm(lm) + mp.flight_eval.read_flight_plan(lm) + mp.flight_eval.calc_flight_performance() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_04.py b/tests/mission/mission_integration_test_04.py index c24ce7c..96082a4 100644 --- a/tests/mission/mission_integration_test_04.py +++ b/tests/mission/mission_integration_test_04.py @@ -1,46 +1,46 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_04.py -# ======================== -# Test case to analyze notional (1D transation + rotation) approach. (NEEDS TLC) - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class OneDimRotApproach(unittest.TestCase): - def test_1d_rot_approach_performance(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration data from text file - lm.set_thruster_config() - - # Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - lm.assign_thruster_groups() - - # Calculate simple 1D flight performance - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - mp.set_lm(lm) - mp.flight_eval.read_flight_plan(lm) - mp.flight_eval.calc_flight_performance() - - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_04.py +# ======================== +# Test case to analyze notional (1D transation + rotation) approach. (NEEDS TLC) + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class OneDimRotApproach(unittest.TestCase): + def test_1d_rot_approach_performance(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration data from text file + lm.set_thruster_config() + + # Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + lm.assign_thruster_groups() + + # Calculate simple 1D flight performance + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + mp.set_lm(lm) + mp.flight_eval.read_flight_plan(lm) + mp.flight_eval.calc_flight_performance() + + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_05.py b/tests/mission/mission_integration_test_05.py index e6ffe26..d79334b 100644 --- a/tests/mission/mission_integration_test_05.py +++ b/tests/mission/mission_integration_test_05.py @@ -1,58 +1,58 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_05.py -# ======================== -# Test case to graph a thrust vs time or distance required given design requirements -# and create a flight envelope to establish thrust requirements. - -# Given Reuirements -# 1. Change in velocity (dV and /or dw) -# 2. System mass properties -# 3. Time or distance limits - -# Desired outputs -# 1. Graph Thrust vs Time reuired. -# 2. Graph Thrust vs Distance required. -# 3. Use time and distance limits to create flight envelope data. -# 4. Add data points for relevant thruster technologies. - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class ThrustEnvelopeChecks(unittest.TestCase): - def test_thrust_envelope_plot(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration data from text file - lm.set_thruster_config() - - # Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - lm.assign_thruster_groups() - - # Read in flight data and plot delta mass contoured for various Δv requirements. - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - mp.set_lm(lm) - mp.flight_eval.read_flight_plan(lm) - # mp.plot_thrust_envelope() - - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_05.py +# ======================== +# Test case to graph a thrust vs time or distance required given design requirements +# and create a flight envelope to establish thrust requirements. + +# Given Reuirements +# 1. Change in velocity (dV and /or dw) +# 2. System mass properties +# 3. Time or distance limits + +# Desired outputs +# 1. Graph Thrust vs Time reuired. +# 2. Graph Thrust vs Distance required. +# 3. Use time and distance limits to create flight envelope data. +# 4. Add data points for relevant thruster technologies. + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class ThrustEnvelopeChecks(unittest.TestCase): + def test_thrust_envelope_plot(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration data from text file + lm.set_thruster_config() + + # Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + lm.assign_thruster_groups() + + # Read in flight data and plot delta mass contoured for various Δv requirements. + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + mp.set_lm(lm) + mp.flight_eval.read_flight_plan(lm) + # mp.plot_thrust_envelope() + + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_06.py b/tests/mission/mission_integration_test_06.py index caef5df..f78822d 100644 --- a/tests/mission/mission_integration_test_06.py +++ b/tests/mission/mission_integration_test_06.py @@ -1,59 +1,59 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_06.py -# ======================== -# Test case to graph a thrust vs time or distance required given design requirements -# and create a flight envelope to establish thrust requirements. - -# Given Reuirements -# 1. Change in velocity (dV and /or dw) -# 2. System mass properties -# 3. Time or distance limits - -# Desired outputs -# TODO: these are bad. need to think about them more. -# 1. Graph ISP vs Fuel reuired. -# 2. Graph Thrust vs Distance required. -# 3. Use time and distance limits to create flight envelope data. -# 4. Add data points for relevant thruster technologies. -# 5. Given a delta V and W requirement, comparison of thrust. isp, and mass flow rate. (M3) (Fuel Usage) - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class DeltaMassChecks(unittest.TestCase): - def test_delta_m_plots(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration data from text file - lm.set_thruster_config() - - # Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - lm.assign_thruster_groups() - - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - mp.set_lm(lm) - mp.flight_eval.read_flight_plan(lm) - - # mp.plot_delta_mass(1885) - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_06.py +# ======================== +# Test case to graph a thrust vs time or distance required given design requirements +# and create a flight envelope to establish thrust requirements. + +# Given Reuirements +# 1. Change in velocity (dV and /or dw) +# 2. System mass properties +# 3. Time or distance limits + +# Desired outputs +# TODO: these are bad. need to think about them more. +# 1. Graph ISP vs Fuel reuired. +# 2. Graph Thrust vs Distance required. +# 3. Use time and distance limits to create flight envelope data. +# 4. Add data points for relevant thruster technologies. +# 5. Given a delta V and W requirement, comparison of thrust. isp, and mass flow rate. (M3) (Fuel Usage) + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class DeltaMassChecks(unittest.TestCase): + def test_delta_m_plots(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration data from text file + lm.set_thruster_config() + + # Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + lm.assign_thruster_groups() + + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + mp.set_lm(lm) + mp.flight_eval.read_flight_plan(lm) + + # mp.plot_delta_mass(1885) + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_07.py b/tests/mission/mission_integration_test_07.py index fc6d83b..d818cc3 100644 --- a/tests/mission/mission_integration_test_07.py +++ b/tests/mission/mission_integration_test_07.py @@ -1,46 +1,46 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_07.py -# ======================== -# Test case to contour the burn plot graph across various thrust and ISP values. (NEEDS TLC) - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class BurnTimeContourChecks(unittest.TestCase): - def test_burn_time_contour_plots(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration data from text file - lm.set_thruster_config() - - # Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - lm.assign_thruster_groups() - - # Read in flight data and plot burntime for a given Δv requirement. - # Graph is contoured according to various ISP values. - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - mp.set_lm(lm) - mp.flight_eval.read_flight_plan(lm) - # mp.plot_burn_time_contour(1194) - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_07.py +# ======================== +# Test case to contour the burn plot graph across various thrust and ISP values. (NEEDS TLC) + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class BurnTimeContourChecks(unittest.TestCase): + def test_burn_time_contour_plots(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration data from text file + lm.set_thruster_config() + + # Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + lm.assign_thruster_groups() + + # Read in flight data and plot burntime for a given Δv requirement. + # Graph is contoured according to various ISP values. + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + mp.set_lm(lm) + mp.flight_eval.read_flight_plan(lm) + # mp.plot_burn_time_contour(1194) + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_integration_test_08.py b/tests/mission/mission_integration_test_08.py index 390aed7..ecc3450 100644 --- a/tests/mission/mission_integration_test_08.py +++ b/tests/mission/mission_integration_test_08.py @@ -1,42 +1,42 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/mission/mission_integration_test_08.py -# ======================== -# Test case to contour the propellant usage across the various Δv in a given flight plan. - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class DeltaMassContourChecks(unittest.TestCase): - def test_delta_m_plots(self): - - # set case directory - case_dir = '../case/mission/flight_envelopes/' - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distrubtion properties. - m = 0.45*30000 # lb converted to kg - h = 14 # m - r = 4.0/2.0 # m - lm.set_inertial_props(m, h, r) - - # Draco/Hypergolic thrusters - lm.add_thruster_performance(400, 300) - lm.assign_thruster_groups() - - # Read in flight data and plot delta mass contoured for various Δv requirements. - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - mp.set_lm(lm) - mp.flight_eval.read_flight_plan(lm) - # mp.plot_delta_mass_contour() - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/mission/mission_integration_test_08.py +# ======================== +# Test case to contour the propellant usage across the various Δv in a given flight plan. + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class DeltaMassContourChecks(unittest.TestCase): + def test_delta_m_plots(self): + + # set case directory + case_dir = '../case/mission/flight_envelopes/' + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distrubtion properties. + m = 0.45*30000 # lb converted to kg + h = 14 # m + r = 4.0/2.0 # m + lm.set_inertial_props(m, h, r) + + # Draco/Hypergolic thrusters + lm.add_thruster_performance(400, 300) + lm.assign_thruster_groups() + + # Read in flight data and plot delta mass contoured for various Δv requirements. + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + mp.set_lm(lm) + mp.flight_eval.read_flight_plan(lm) + # mp.plot_delta_mass_contour() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/mission/mission_unit_test_01.py b/tests/mission/mission_unit_test_01.py index 041e405..e146e95 100644 --- a/tests/mission/mission_unit_test_01.py +++ b/tests/mission/mission_unit_test_01.py @@ -1,20 +1,20 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/mission/mission_unit_test_01.py -# ======================== -# Write test case description. - -import unittest - -class MDAOTest(unittest.TestCase): - def test_mission(self): - # print("mission unit test") - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/mission/mission_unit_test_01.py +# ======================== +# Write test case description. + +import unittest + +class MDAOTest(unittest.TestCase): + def test_mission(self): + # print("mission unit test") + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mission/mission_verification_test_01.py b/tests/mission/mission_verification_test_01.py index ca3ff9e..67bb77d 100644 --- a/tests/mission/mission_verification_test_01.py +++ b/tests/mission/mission_verification_test_01.py @@ -1,21 +1,21 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/mission/mission_verification_test_01.py -# ======================== -# Write test case description. - - -import unittest - -class MDAOTest(unittest.TestCase): - def test_mission(self): - # print("mission verification test") - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/mission/mission_verification_test_01.py +# ======================== +# Write test case description. + + +import unittest + +class MDAOTest(unittest.TestCase): + def test_mission(self): + # print("mission verification test") + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/mission/mission_verification_test_02.py b/tests/mission/mission_verification_test_02.py index 25305ba..5bc08de 100644 --- a/tests/mission/mission_verification_test_02.py +++ b/tests/mission/mission_verification_test_02.py @@ -1,30 +1,30 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/mission/mission_verification_test_01.py -# ======================== -# Write test case description. - - -import unittest -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class MDAOTest(unittest.TestCase): - def test_mission(self): - case_dir = '../case/mission/flight_envelopes/' - me = MissionEnvironment.MissionEnvironment(case_dir) - planner = MissionPlanner.MissionPlanner(me) - planner.orbital_transfer.init_hohmann_transfers() - planner.orbital_transfer.add_hohmann_transfer(300, 20000, leg_id="LEO to MEO") - planner.orbital_transfer.add_hohmann_transfer(20000, 35786, leg_id="MEO to GEO") - planner.orbital_transfer.summarize_hohmann_transfers() - - - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/mission/mission_verification_test_01.py +# ======================== +# Write test case description. + + +import unittest +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class MDAOTest(unittest.TestCase): + def test_mission(self): + case_dir = '../case/mission/flight_envelopes/' + me = MissionEnvironment.MissionEnvironment(case_dir) + planner = MissionPlanner.MissionPlanner(me) + planner.orbital_transfer.init_hohmann_transfers() + planner.orbital_transfer.add_hohmann_transfer(300, 20000, leg_id="LEO to MEO") + planner.orbital_transfer.add_hohmann_transfer(20000, 35786, leg_id="MEO to GEO") + planner.orbital_transfer.summarize_hohmann_transfers() + + + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/old/test_case_15.py b/tests/old/test_case_15.py index 3609fc2..28e88b1 100644 --- a/tests/old/test_case_15.py +++ b/tests/old/test_case_15.py @@ -1,40 +1,40 @@ -# Juan P. Roldan -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 02-06-24 - -# ======================== -# PyRPOD: tests/test_case_15.py -# ======================== -# Test case for testing plume gas kinetic models in jfh firings. - -import test_header -import unittest, os, sys -from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD - -class LoadJFHChecks(unittest.TestCase): - def test_plume_kinetics(self): - - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/plume_case/' - - # Load JFH data. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - vv = VisitingVehicle.VisitingVehicle(case_dir) - vv.set_stl() - vv.set_thruster_config() - vv.set_thruster_metrics() - - rpod = RPOD.RPOD(case_dir) - rpod.study_init(jfh, tv, vv) - - rpod.graph_jfh() - rpod.jfh_plume_strikes() - -if __name__ == '__main__': +# Juan P. Roldan +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 02-06-24 + +# ======================== +# PyRPOD: tests/test_case_15.py +# ======================== +# Test case for testing plume gas kinetic models in jfh firings. + +import test_header +import unittest, os, sys +from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD + +class LoadJFHChecks(unittest.TestCase): + def test_plume_kinetics(self): + + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/plume_case/' + + # Load JFH data. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(case_dir) + vv.set_stl() + vv.set_thruster_config() + vv.set_thruster_metrics() + + rpod = RPOD.RPOD(case_dir) + rpod.study_init(jfh, tv, vv) + + rpod.graph_jfh() + rpod.jfh_plume_strikes() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/old/test_case_17.py b/tests/old/test_case_17.py index d6c91d5..96b39e3 100644 --- a/tests/old/test_case_17.py +++ b/tests/old/test_case_17.py @@ -1,39 +1,39 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-14-23 - -# ======================== -# PyRPOD: test/test_case_13.py -# ======================== -# Test case for converting STL data to VTK data. -# This is accomplished by checking for the proper data format of VTK files. - -import test_header -import unittest, os, sys -from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD - -class LoadJFHChecks(unittest.TestCase): - def test_jfh_reader(self): - - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/base_case/' - - # Load JFH data. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - vv = VisitingVehicle.VisitingVehicle(case_dir) - vv.set_stl() - vv.set_thruster_config() - - rpod = RPOD.RPOD(case_dir) - rpod.study_init(jfh, tv, vv) - rpod.graph_jfh() - rpod.jfh_plume_strikes() - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-14-23 + +# ======================== +# PyRPOD: test/test_case_13.py +# ======================== +# Test case for converting STL data to VTK data. +# This is accomplished by checking for the proper data format of VTK files. + +import test_header +import unittest, os, sys +from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD + +class LoadJFHChecks(unittest.TestCase): + def test_jfh_reader(self): + + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/base_case/' + + # Load JFH data. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(case_dir) + vv.set_stl() + vv.set_thruster_config() + + rpod = RPOD.RPOD(case_dir) + rpod.study_init(jfh, tv, vv) + rpod.graph_jfh() + rpod.jfh_plume_strikes() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/old/test_case_19.py b/tests/old/test_case_19.py index d17877e..f10187e 100644 --- a/tests/old/test_case_19.py +++ b/tests/old/test_case_19.py @@ -1,44 +1,44 @@ -# Andy Torres, Pearce Patterson, Nicholas A. Palumbo -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 1-29-24 - -# ======================== -# PyRPOD: test/test_case_19.py -# ======================== -# Test case for producing hollow cube data. - -import test_header -import unittest, os, sys -from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD - -class LoadJFHChecks(unittest.TestCase): - def test_hollow_cube(self): - - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/hollow_cube/' - - # Load JFH data. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - # Load Target Vehicle. - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - # Load Visiting Vehicle. - vv = VisitingVehicle.VisitingVehicle(case_dir) - vv.set_stl() - vv.set_thruster_config() - # vv.set_thruster_metrics() - - # Initiate RPOD study. - rpod = RPOD.RPOD(case_dir) - rpod.study_init(jfh, tv, vv) - - # Run plume strike analysis - rpod.graph_jfh() - rpod.jfh_plume_strikes() - -if __name__ == '__main__': +# Andy Torres, Pearce Patterson, Nicholas A. Palumbo +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 1-29-24 + +# ======================== +# PyRPOD: test/test_case_19.py +# ======================== +# Test case for producing hollow cube data. + +import test_header +import unittest, os, sys +from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD + +class LoadJFHChecks(unittest.TestCase): + def test_hollow_cube(self): + + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/hollow_cube/' + + # Load JFH data. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + # Load Target Vehicle. + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + # Load Visiting Vehicle. + vv = VisitingVehicle.VisitingVehicle(case_dir) + vv.set_stl() + vv.set_thruster_config() + # vv.set_thruster_metrics() + + # Initiate RPOD study. + rpod = RPOD.RPOD(case_dir) + rpod.study_init(jfh, tv, vv) + + # Run plume strike analysis + rpod.graph_jfh() + rpod.jfh_plume_strikes() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/plume/plume_integration_test_01.py b/tests/plume/plume_integration_test_01.py index 5906e0e..043c281 100644 --- a/tests/plume/plume_integration_test_01.py +++ b/tests/plume/plume_integration_test_01.py @@ -1,20 +1,20 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/plume/plume_integration_test_01.py -# ======================== -# Write test case description. - -import unittest - -class MDAOTest(unittest.TestCase): - def test_mdao(self): - # print("plume integration test") - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/plume/plume_integration_test_01.py +# ======================== +# Write test case description. + +import unittest + +class MDAOTest(unittest.TestCase): + def test_mdao(self): + # print("plume integration test") + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/plume/plume_unit_test_01.py b/tests/plume/plume_unit_test_01.py index a11712e..b42cb92 100644 --- a/tests/plume/plume_unit_test_01.py +++ b/tests/plume/plume_unit_test_01.py @@ -1,20 +1,20 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-05-23 - - -# ======================== -# PyRPOD: test/plume/plume_unit_test_01.py -# ======================== -# Write test case description. - -import unittest - -class PlumeTest(unittest.TestCase): - def test_plume(self): - # print("plume unit test") - return - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-05-23 + + +# ======================== +# PyRPOD: test/plume/plume_unit_test_01.py +# ======================== +# Write test case description. + +import unittest + +class PlumeTest(unittest.TestCase): + def test_plume(self): + # print("plume unit test") + return + +if __name__ == '__main__': + unittest.main() diff --git a/tests/plume/plume_verification_test_01.py b/tests/plume/plume_verification_test_01.py index 9cdc459..2b75412 100644 --- a/tests/plume/plume_verification_test_01.py +++ b/tests/plume/plume_verification_test_01.py @@ -1,31 +1,31 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/plume/plume_verification_test_01.py -# ======================== -# A test case to plot simple radial expansion profiles. -# TODO: Re-factor code to save data in a relevant object. Also add files to save to. - -import unittest, os, sys -from pyrpod.plume import IsentropicExpansion - -class IsentropicExpansionCheck(unittest.TestCase): - def test_temp_vs_radial_expansion(self): - - #define flow and sonic properties. - M1 = 1 - M2 = 25 - gamma = 5/3 - T_star = 500 - r_star = 1 - - # Plot isentropic expansion curves. - isen_plume = IsentropicExpansion.IsentropicExpansion() - # isen_plume.plot_number_density_ratios_vs_radius(M1, M2, gamma, r_star) - # isen_plume.plot_temp_ratios_vs_radius(M1, M2, gamma, r_star) - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/plume/plume_verification_test_01.py +# ======================== +# A test case to plot simple radial expansion profiles. +# TODO: Re-factor code to save data in a relevant object. Also add files to save to. + +import unittest, os, sys +from pyrpod.plume import IsentropicExpansion + +class IsentropicExpansionCheck(unittest.TestCase): + def test_temp_vs_radial_expansion(self): + + #define flow and sonic properties. + M1 = 1 + M2 = 25 + gamma = 5/3 + T_star = 500 + r_star = 1 + + # Plot isentropic expansion curves. + isen_plume = IsentropicExpansion.IsentropicExpansion() + # isen_plume.plot_number_density_ratios_vs_radius(M1, M2, gamma, r_star) + # isen_plume.plot_temp_ratios_vs_radius(M1, M2, gamma, r_star) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/rpod/rpod_integration_test_01.py b/tests/rpod/rpod_integration_test_01.py index 32f02e2..93eca6a 100644 --- a/tests/rpod/rpod_integration_test_01.py +++ b/tests/rpod/rpod_integration_test_01.py @@ -1,143 +1,143 @@ -import logging -logging.basicConfig(filename='rpod_integration_test_01.log', level=logging.INFO, format='%(message)s') - -# Andy Torres, Nicholas Palumbo -# Last Changed: 11-17-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_integration_test_01.py -# ======================== -# This test asserts the expected number of cell strikes on a flat plate STL -# for a notional trajectory meant to reperesent a "sweep" above it. This -# is also established as the base case for RPOD plume impingement analysis. -# The test uses JFH data to assert expected strike counts across 20 distinct firings. - -import unittest, os, sys -from pyrpod.vehicle import LogisticsModule, TargetVehicle -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mission import MissionEnvironment - -class BaseCaseChecks(unittest.TestCase): - def test_base_case(self): - - # 1. Set Up - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/rpod/base_case/' - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - - # Load Target Vehicle. - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Load in thruster configuration file. - lm.set_thruster_config() - - # Set mission environment. - me = MissionEnvironment.MissionEnvironment(case_dir) - - # Instantiate RPOD object. - plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) - plume_strike_study.study_init(jfh, tv, lm) - - # Read in JFH. - jfh.read_jfh() - - # 2. Execute - # Conduct RPOD analysis - plume_strike_study.graph_jfh() - strikes = plume_strike_study.jfh_plume_strikes() - - # 3. Assert - # Assert expected strike values for each firing in the JFH. - expected_strikes = { - '1': 282.0, - '2': 279.0, - '3': 285.0, - '4': 282.0, - '5': 280.0, - '6': 284.0, - '7': 281.0, - '8': 280.0, - '9': 282.0, - '10': 280.0, - '11': 280.0, - '12': 277.0, - '13': 283.0, - '14': 282.0, - '15': 279.0, - '16': 285.0, - '17': 282.0, - '18': 280.0, - '19': 284.0, - '20': 282.0 - } - - # Assert expected cumulative strike values for each firing in the JFH. - expected_cum_strikes = { - '1': 282.0, - '2': 561.0, - '3': 846.0, - '4': 1128.0, - '5': 1408.0, - '6': 1692.0, - '7': 1973.0, - '8': 2253.0, - '9': 2535.0, - '10': 2815.0, - '11': 3095.0, - '12': 3372.0, - '13': 3655.0, - '14': 3937.0, - '15': 4216.0, - '16': 4501.0, - '17': 4783.0, - '18': 5063.0, - '19': 5347.0, - '20': 5629.0 - } - - # Read in expected strikes from text file. - file_path = 'rpod/rpod_int_test_01_expected_strikes.log' - expected_strike_ids = {} - with open(file_path, 'r') as file: - file_content = file.readlines() - - cur_firing = '' - - for line in file_content: - # Make a an array of data to orgnaize strike data by firing. - if 'n_firing' in line: - cur_firing = str(line.split()[1]) - expected_strike_ids[cur_firing] = [] - else: - expected_strike_ids[cur_firing].append(int(line)) - - for n_firing in strikes.keys(): - # Development statements used to write comparison entries in expected_strikes - # logging.info('n_firing ' + str(n_firing)) - for i in range(len(strikes[n_firing]['strikes'])): - if strikes[n_firing]['strikes'][i] > 0: - # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) - # logging.info(string) - - # logging.info(str(i)) - self.assertIn(i, expected_strike_ids[n_firing]) - # Development statements used to write comparison entries in expected_strikes - string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['cum_strikes'].sum()) +',' - logging.info(string) - - # Number of strikes for a given time step. - n_strikes = strikes[n_firing]['strikes'].sum() - n_cum_strikes = strikes[n_firing]['cum_strikes'].sum() - - # Assert that it matches the expected value. - self.assertEqual(n_strikes, expected_strikes[n_firing]) - self.assertEqual(n_cum_strikes, expected_cum_strikes[n_firing]) - -if __name__ == '__main__': +import logging +logging.basicConfig(filename='rpod_integration_test_01.log', level=logging.INFO, format='%(message)s') + +# Andy Torres, Nicholas Palumbo +# Last Changed: 11-17-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_integration_test_01.py +# ======================== +# This test asserts the expected number of cell strikes on a flat plate STL +# for a notional trajectory meant to reperesent a "sweep" above it. This +# is also established as the base case for RPOD plume impingement analysis. +# The test uses JFH data to assert expected strike counts across 20 distinct firings. + +import unittest, os, sys +from pyrpod.vehicle import LogisticsModule, TargetVehicle +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mission import MissionEnvironment + +class BaseCaseChecks(unittest.TestCase): + def test_base_case(self): + + # 1. Set Up + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/rpod/base_case/' + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + + # Load Target Vehicle. + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Load in thruster configuration file. + lm.set_thruster_config() + + # Set mission environment. + me = MissionEnvironment.MissionEnvironment(case_dir) + + # Instantiate RPOD object. + plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + plume_strike_study.study_init(jfh, tv, lm) + + # Read in JFH. + jfh.read_jfh() + + # 2. Execute + # Conduct RPOD analysis + plume_strike_study.graph_jfh() + strikes = plume_strike_study.jfh_plume_strikes() + + # 3. Assert + # Assert expected strike values for each firing in the JFH. + expected_strikes = { + '1': 282.0, + '2': 279.0, + '3': 285.0, + '4': 282.0, + '5': 280.0, + '6': 284.0, + '7': 281.0, + '8': 280.0, + '9': 282.0, + '10': 280.0, + '11': 280.0, + '12': 277.0, + '13': 283.0, + '14': 282.0, + '15': 279.0, + '16': 285.0, + '17': 282.0, + '18': 280.0, + '19': 284.0, + '20': 282.0 + } + + # Assert expected cumulative strike values for each firing in the JFH. + expected_cum_strikes = { + '1': 282.0, + '2': 561.0, + '3': 846.0, + '4': 1128.0, + '5': 1408.0, + '6': 1692.0, + '7': 1973.0, + '8': 2253.0, + '9': 2535.0, + '10': 2815.0, + '11': 3095.0, + '12': 3372.0, + '13': 3655.0, + '14': 3937.0, + '15': 4216.0, + '16': 4501.0, + '17': 4783.0, + '18': 5063.0, + '19': 5347.0, + '20': 5629.0 + } + + # Read in expected strikes from text file. + file_path = 'rpod/rpod_int_test_01_expected_strikes.log' + expected_strike_ids = {} + with open(file_path, 'r') as file: + file_content = file.readlines() + + cur_firing = '' + + for line in file_content: + # Make a an array of data to orgnaize strike data by firing. + if 'n_firing' in line: + cur_firing = str(line.split()[1]) + expected_strike_ids[cur_firing] = [] + else: + expected_strike_ids[cur_firing].append(int(line)) + + for n_firing in strikes.keys(): + # Development statements used to write comparison entries in expected_strikes + # logging.info('n_firing ' + str(n_firing)) + for i in range(len(strikes[n_firing]['strikes'])): + if strikes[n_firing]['strikes'][i] > 0: + # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) + # logging.info(string) + + # logging.info(str(i)) + self.assertIn(i, expected_strike_ids[n_firing]) + # Development statements used to write comparison entries in expected_strikes + string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['cum_strikes'].sum()) +',' + logging.info(string) + + # Number of strikes for a given time step. + n_strikes = strikes[n_firing]['strikes'].sum() + n_cum_strikes = strikes[n_firing]['cum_strikes'].sum() + + # Assert that it matches the expected value. + self.assertEqual(n_strikes, expected_strikes[n_firing]) + self.assertEqual(n_cum_strikes, expected_cum_strikes[n_firing]) + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod/rpod_integration_test_02.py b/tests/rpod/rpod_integration_test_02.py index db5c69f..f8a60c2 100644 --- a/tests/rpod/rpod_integration_test_02.py +++ b/tests/rpod/rpod_integration_test_02.py @@ -1,147 +1,147 @@ -import logging -logging.basicConfig(filename='rpod_integration_test_02.log', level=logging.INFO, format='%(message)s') - -# Andy Torres, Nicholas Palumbo -# Last Changed: 11-10-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_integration_test_02.py -# ======================== -# This test asserts the expected number of cell strikes on a flat plate STL -# as a notional VV approaches it via a direct trajectory solved using 1D physics. This 1D -# trajectory reperesents a VV firing its adverse thrusters to slow down in preperation for docking. -# The test uses JFH data to assert expected strike counts across 15 distinct firings. - -import unittest, os, sys - -from pyrpod.vehicle import LogisticsModule, TargetVehicle -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mission import MissionEnvironment - -class OneDimTransApproachChecks(unittest.TestCase): - def test_1d_approach(self): - - # 1. Set Up - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/rpod/1d_approach/' - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - - # Load Target Vehicle. - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Define LM mass distribution properties. - m = 14000 # kg - h = 11 # m - r = 2 # m - lm.set_inertial_props(m, h, r) - - # Load in thruster configuration file. - lm.set_thruster_config() - # Load in thruster data file - lm.set_thruster_metrics() - # Use TCD to group DOF - lm.assign_thruster_groups() - - # Set mission environment. - me = MissionEnvironment.MissionEnvironment(case_dir) - - # Instantiate RPOD object. - plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) - plume_strike_study.study_init(jfh, tv, lm) - - # Produce JFH using 1D physics - # r_o = 40 # initial distance (m) - # v_o = 0.2 # Initial velocity (m/s) - # v_ida = 0.03 # Docking velocity (m/s) - # rpod.print_jfh_1d_approach(v_ida, v_o, r_o) - - # Read in JFH. - jfh.read_jfh() - - # 2. Execute - # Conduct RPOD analysis - plume_strike_study.graph_jfh() - strikes = plume_strike_study.jfh_plume_strikes() - - # logging.info(len(strikes['1']['cum_strikes'])) - - # 3. Assert - # Assert expected strike values for each firing in the JFH. - expected_strikes = { - '1': 1492.0, - '2': 1368.0, - '3': 1138.0, - '4': 864.0, - '5': 632.0, - '6': 428.0, - '7': 276.0, - '8': 150.0, - '9': 64.0, - '10': 16.0 - } - - # Assert expected cumulative strike values for each firing in the JFH. - expected_cum_strikes = { - '1': 1492.0, - '2': 2860.0, - '3': 3998.0, - '4': 4862.0, - '5': 5494.0, - '6': 5922.0, - '7': 6198.0, - '8': 6348.0, - '9': 6412.0, - '10': 6428.0 - } - - # Read in expected strikes from text file. - file_path = 'rpod/rpod_int_test_02_expected_strikes.log' - expected_strike_ids = {} - with open(file_path, 'r') as file: - file_content = file.readlines() - - cur_firing = '' - - for line in file_content: - # Make a an array of data to orgnaize strike data by firing. - if 'n_firing' in line: - cur_firing = str(line.split()[1]) - expected_strike_ids[cur_firing] = [] - else: - expected_strike_ids[cur_firing].append(int(line)) - - for n_firing in strikes.keys(): - # Development statements used to write comparison entries in expected_strikes - # logging.info('n_firing ' + str(n_firing)) - for i in range(len(strikes[n_firing]['strikes'])): - if strikes[n_firing]['strikes'][i] > 0: - # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) - # # logging.info(string) - - # logging.info(str(i)) - - # assert that the tracking of each face strike is enough. - self.assertIn(i, expected_strike_ids[n_firing]) - - # Development statements used to write comparison entries in expected_strikes - # string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['strikes'].sum()) +',' - # logging.info(string) - - # Number of strikes for a given time step. - n_strikes = strikes[n_firing]['strikes'].sum() - n_cum_strikes = strikes[n_firing]['cum_strikes'].sum() - - # logging.info('n_firing ' + str(n_firing)) - - # Assert that it matches the expected value. - self.assertEqual(n_strikes, expected_strikes[n_firing]) - self.assertEqual(n_cum_strikes, expected_cum_strikes[n_firing]) - -if __name__ == '__main__': +import logging +logging.basicConfig(filename='rpod_integration_test_02.log', level=logging.INFO, format='%(message)s') + +# Andy Torres, Nicholas Palumbo +# Last Changed: 11-10-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_integration_test_02.py +# ======================== +# This test asserts the expected number of cell strikes on a flat plate STL +# as a notional VV approaches it via a direct trajectory solved using 1D physics. This 1D +# trajectory reperesents a VV firing its adverse thrusters to slow down in preperation for docking. +# The test uses JFH data to assert expected strike counts across 15 distinct firings. + +import unittest, os, sys + +from pyrpod.vehicle import LogisticsModule, TargetVehicle +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mission import MissionEnvironment + +class OneDimTransApproachChecks(unittest.TestCase): + def test_1d_approach(self): + + # 1. Set Up + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/rpod/1d_approach/' + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + + # Load Target Vehicle. + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Define LM mass distribution properties. + m = 14000 # kg + h = 11 # m + r = 2 # m + lm.set_inertial_props(m, h, r) + + # Load in thruster configuration file. + lm.set_thruster_config() + # Load in thruster data file + lm.set_thruster_metrics() + # Use TCD to group DOF + lm.assign_thruster_groups() + + # Set mission environment. + me = MissionEnvironment.MissionEnvironment(case_dir) + + # Instantiate RPOD object. + plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + plume_strike_study.study_init(jfh, tv, lm) + + # Produce JFH using 1D physics + # r_o = 40 # initial distance (m) + # v_o = 0.2 # Initial velocity (m/s) + # v_ida = 0.03 # Docking velocity (m/s) + # rpod.print_jfh_1d_approach(v_ida, v_o, r_o) + + # Read in JFH. + jfh.read_jfh() + + # 2. Execute + # Conduct RPOD analysis + plume_strike_study.graph_jfh() + strikes = plume_strike_study.jfh_plume_strikes() + + # logging.info(len(strikes['1']['cum_strikes'])) + + # 3. Assert + # Assert expected strike values for each firing in the JFH. + expected_strikes = { + '1': 1492.0, + '2': 1368.0, + '3': 1138.0, + '4': 864.0, + '5': 632.0, + '6': 428.0, + '7': 276.0, + '8': 150.0, + '9': 64.0, + '10': 16.0 + } + + # Assert expected cumulative strike values for each firing in the JFH. + expected_cum_strikes = { + '1': 1492.0, + '2': 2860.0, + '3': 3998.0, + '4': 4862.0, + '5': 5494.0, + '6': 5922.0, + '7': 6198.0, + '8': 6348.0, + '9': 6412.0, + '10': 6428.0 + } + + # Read in expected strikes from text file. + file_path = 'rpod/rpod_int_test_02_expected_strikes.log' + expected_strike_ids = {} + with open(file_path, 'r') as file: + file_content = file.readlines() + + cur_firing = '' + + for line in file_content: + # Make a an array of data to orgnaize strike data by firing. + if 'n_firing' in line: + cur_firing = str(line.split()[1]) + expected_strike_ids[cur_firing] = [] + else: + expected_strike_ids[cur_firing].append(int(line)) + + for n_firing in strikes.keys(): + # Development statements used to write comparison entries in expected_strikes + # logging.info('n_firing ' + str(n_firing)) + for i in range(len(strikes[n_firing]['strikes'])): + if strikes[n_firing]['strikes'][i] > 0: + # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) + # # logging.info(string) + + # logging.info(str(i)) + + # assert that the tracking of each face strike is enough. + self.assertIn(i, expected_strike_ids[n_firing]) + + # Development statements used to write comparison entries in expected_strikes + # string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['strikes'].sum()) +',' + # logging.info(string) + + # Number of strikes for a given time step. + n_strikes = strikes[n_firing]['strikes'].sum() + n_cum_strikes = strikes[n_firing]['cum_strikes'].sum() + + # logging.info('n_firing ' + str(n_firing)) + + # Assert that it matches the expected value. + self.assertEqual(n_strikes, expected_strikes[n_firing]) + self.assertEqual(n_cum_strikes, expected_cum_strikes[n_firing]) + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod/rpod_integration_test_03.py b/tests/rpod/rpod_integration_test_03.py index b1f4ff7..c11ae77 100644 --- a/tests/rpod/rpod_integration_test_03.py +++ b/tests/rpod/rpod_integration_test_03.py @@ -1,134 +1,134 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_integration_test_03.py -# ======================== -# Test case to analyze Keep Out Zone Impingement. (WIP) - -import logging -logging.basicConfig(filename='rpod_integration_test_03.log', level=logging.INFO, format='%(message)s') - - -import unittest, os, sys - -from pyrpod.vehicle import LogisticsModule, TargetVehicle -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mission import MissionEnvironment - -class KeepOutZoneChecks(unittest.TestCase): - def test_keep_out_zone(self): - - # 1. Set Up - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/rpod/koz/' - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - - # Load Target Vehicle. - tv = TargetVehicle.TargetVehicle(case_dir) - # print(tv.config.items) - - tv.set_stl() - - # Instantiate LogisticModule object. - lm = LogisticsModule.LogisticsModule(case_dir) - - # Load in thruster configuration file. - lm.set_thruster_config() - - # Set mission environment. - me = MissionEnvironment.MissionEnvironment(case_dir) - - # Instantiate RPOD object. - plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) - plume_strike_study.study_init(jfh, tv, lm) - - # Read in JFH. - jfh.read_jfh() - - # 2. Execute - # Conduct RPOD analysis - plume_strike_study.graph_jfh() - strikes = plume_strike_study.jfh_plume_strikes() - - # 3. Assert - # Read in expected strikes from text file. - expected_strikes = { - '1': 147.0, - '2': 118.0, - '3': 92.0, - '4': 67.0, - '5': 45.0, - '6': 32.0, - '7': 21.0, - '8': 10.0, - '9': 5.0, - '10': 1.0 - } - - expected_cum_strikes = { - '1': 147.0, - '2': 265.0, - '3': 357.0, - '4': 424.0, - '5': 469.0, - '6': 501.0, - '7': 522.0, - '8': 532.0, - '9': 537.0, - '10': 538.0 - } - - file_path = 'rpod/rpod_int_test_03_expected_strikes.log' - expected_strike_ids = {} - with open(file_path, 'r') as file: - file_content = file.readlines() - - cur_firing = '' - - for line in file_content: - # Make a an array of data to orgnaize strike data by firing. - if 'n_firing' in line: - cur_firing = line.split()[1] - expected_strike_ids[cur_firing] = [] - # Append index values of the current cell to 'n_firing' array. - else: - expected_strike_ids[cur_firing].append(int(line)) - - # # Development statements used to write comparison entries in expected_strikes - for n_firing in strikes.keys(): - # logging.info('n_firing ' + str(n_firing)) - # for i in range(len(strikes[n_firing]['strikes'])): - # if strikes[n_firing]['strikes'][i] > 0: - # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) - # # logging.info(string) - - # # logging.info(str(i)) - - # Sums the total amount of cell strikes per firing. Saves data to a dictionary. - string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['cum_strikes'].sum()) +',' - # logging.info(string) - - # Assert that each firing is striking the expected cells by comparing index values. - for i in range(len(strikes[n_firing]['strikes'])): - if strikes[n_firing]['strikes'][i] > 0: - self.assertIn(i, expected_strike_ids[n_firing]) - - # Number of strikes for a given time step. - n_strikes = strikes[n_firing]['strikes'].sum() - n_cum_strikes = strikes[n_firing]['cum_strikes'].sum() - # logging.info('n_strikes ' + str(n_strikes)) - - # Assert that it matches the expected value. - # print(type(expected_strikes[n_firing]), expected_strikes[n_firing]) - self.assertEqual(n_strikes, expected_strikes[n_firing]) - self.assertEqual(n_cum_strikes, expected_cum_strikes[n_firing]) - - return - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_integration_test_03.py +# ======================== +# Test case to analyze Keep Out Zone Impingement. (WIP) + +import logging +logging.basicConfig(filename='rpod_integration_test_03.log', level=logging.INFO, format='%(message)s') + + +import unittest, os, sys + +from pyrpod.vehicle import LogisticsModule, TargetVehicle +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mission import MissionEnvironment + +class KeepOutZoneChecks(unittest.TestCase): + def test_keep_out_zone(self): + + # 1. Set Up + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/rpod/koz/' + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + + # Load Target Vehicle. + tv = TargetVehicle.TargetVehicle(case_dir) + # print(tv.config.items) + + tv.set_stl() + + # Instantiate LogisticModule object. + lm = LogisticsModule.LogisticsModule(case_dir) + + # Load in thruster configuration file. + lm.set_thruster_config() + + # Set mission environment. + me = MissionEnvironment.MissionEnvironment(case_dir) + + # Instantiate RPOD object. + plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + plume_strike_study.study_init(jfh, tv, lm) + + # Read in JFH. + jfh.read_jfh() + + # 2. Execute + # Conduct RPOD analysis + plume_strike_study.graph_jfh() + strikes = plume_strike_study.jfh_plume_strikes() + + # 3. Assert + # Read in expected strikes from text file. + expected_strikes = { + '1': 147.0, + '2': 118.0, + '3': 92.0, + '4': 67.0, + '5': 45.0, + '6': 32.0, + '7': 21.0, + '8': 10.0, + '9': 5.0, + '10': 1.0 + } + + expected_cum_strikes = { + '1': 147.0, + '2': 265.0, + '3': 357.0, + '4': 424.0, + '5': 469.0, + '6': 501.0, + '7': 522.0, + '8': 532.0, + '9': 537.0, + '10': 538.0 + } + + file_path = 'rpod/rpod_int_test_03_expected_strikes.log' + expected_strike_ids = {} + with open(file_path, 'r') as file: + file_content = file.readlines() + + cur_firing = '' + + for line in file_content: + # Make a an array of data to orgnaize strike data by firing. + if 'n_firing' in line: + cur_firing = line.split()[1] + expected_strike_ids[cur_firing] = [] + # Append index values of the current cell to 'n_firing' array. + else: + expected_strike_ids[cur_firing].append(int(line)) + + # # Development statements used to write comparison entries in expected_strikes + for n_firing in strikes.keys(): + # logging.info('n_firing ' + str(n_firing)) + # for i in range(len(strikes[n_firing]['strikes'])): + # if strikes[n_firing]['strikes'][i] > 0: + # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) + # # logging.info(string) + + # # logging.info(str(i)) + + # Sums the total amount of cell strikes per firing. Saves data to a dictionary. + string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['cum_strikes'].sum()) +',' + # logging.info(string) + + # Assert that each firing is striking the expected cells by comparing index values. + for i in range(len(strikes[n_firing]['strikes'])): + if strikes[n_firing]['strikes'][i] > 0: + self.assertIn(i, expected_strike_ids[n_firing]) + + # Number of strikes for a given time step. + n_strikes = strikes[n_firing]['strikes'].sum() + n_cum_strikes = strikes[n_firing]['cum_strikes'].sum() + # logging.info('n_strikes ' + str(n_strikes)) + + # Assert that it matches the expected value. + # print(type(expected_strikes[n_firing]), expected_strikes[n_firing]) + self.assertEqual(n_strikes, expected_strikes[n_firing]) + self.assertEqual(n_cum_strikes, expected_cum_strikes[n_firing]) + + return + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod/rpod_integration_test_04.py b/tests/rpod/rpod_integration_test_04.py index cb34183..406065c 100644 --- a/tests/rpod/rpod_integration_test_04.py +++ b/tests/rpod/rpod_integration_test_04.py @@ -1,90 +1,90 @@ -import logging -logging.basicConfig(filename='rpod_integration_test_04.log', level=logging.INFO, format='%(message)s') - -# Andy Torres, Nicholas A. Palumbo -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_verification_test_01.py -# ======================== -# Test case for producing hollow cube data. - -import unittest, os, sys -import pandas as pd - -from pyrpod.vehicle import LogisticsModule, TargetVehicle, VisitingVehicle -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mission import MissionEnvironment - -class HollowCubeChecks(unittest.TestCase): - def test_hollow_cube(self): - - # 1. Set Up - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/rpod/hollow_cube/' - - # Load JFH data. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - # Load Target Vehicle. - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - # Load Visiting Vehicle. - vv = VisitingVehicle.VisitingVehicle(case_dir) - vv.set_stl() - vv.set_thruster_config() - # vv.set_thruster_metrics() - - # Set mission environment. - me = MissionEnvironment.MissionEnvironment(case_dir) - - # Instantiate RPOD object. - plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) - plume_strike_study.study_init(jfh, tv, vv) - - # 2. Execute - # Run plume strike analysis - plume_strike_study.graph_jfh() - strikes = plume_strike_study.jfh_plume_strikes() - - # 3. Assert - # Read in expected strikes from text file. - file_path = 'rpod/rpod_int_test_04_expected_strikes.log' - expected_strikes = {} - with open(file_path, 'r') as file: - file_content = file.readlines() - - cur_firing = '' - - for line in file_content: - # Make a an array of data to orgnaize strike data by firing. - if 'n_firing' in line: - cur_firing = line.split()[1] - expected_strikes[cur_firing] = [] - # Append index values of the current cell to 'n_firing' array. - else: - expected_strikes[cur_firing].append(int(line)) - - # # Development statements used to write comparison entries in expected_strikes - for n_firing in strikes.keys(): - # logging.info('n_firing ' + str(n_firing)) - # for i in range(len(strikes[n_firing]['strikes'])): - # if strikes[n_firing]['strikes'][i] > 0: - # # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) - # # logging.info(string) - - # logging.info(str(i)) - - # Sums the total amount of cell strikes per firing. Saves data to a dictionary. - # string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['strikes'].sum()) +',' - # logging.info(string) - - # Assert that each firing is striking the expected cells by comparing index values. - for i in range(len(strikes[n_firing]['strikes'])): - if strikes[n_firing]['strikes'][i] > 0: - self.assertIn(i, expected_strikes[n_firing]) - -if __name__ == '__main__': +import logging +logging.basicConfig(filename='rpod_integration_test_04.log', level=logging.INFO, format='%(message)s') + +# Andy Torres, Nicholas A. Palumbo +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_verification_test_01.py +# ======================== +# Test case for producing hollow cube data. + +import unittest, os, sys +import pandas as pd + +from pyrpod.vehicle import LogisticsModule, TargetVehicle, VisitingVehicle +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mission import MissionEnvironment + +class HollowCubeChecks(unittest.TestCase): + def test_hollow_cube(self): + + # 1. Set Up + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/rpod/hollow_cube/' + + # Load JFH data. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + # Load Target Vehicle. + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + # Load Visiting Vehicle. + vv = VisitingVehicle.VisitingVehicle(case_dir) + vv.set_stl() + vv.set_thruster_config() + # vv.set_thruster_metrics() + + # Set mission environment. + me = MissionEnvironment.MissionEnvironment(case_dir) + + # Instantiate RPOD object. + plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + plume_strike_study.study_init(jfh, tv, vv) + + # 2. Execute + # Run plume strike analysis + plume_strike_study.graph_jfh() + strikes = plume_strike_study.jfh_plume_strikes() + + # 3. Assert + # Read in expected strikes from text file. + file_path = 'rpod/rpod_int_test_04_expected_strikes.log' + expected_strikes = {} + with open(file_path, 'r') as file: + file_content = file.readlines() + + cur_firing = '' + + for line in file_content: + # Make a an array of data to orgnaize strike data by firing. + if 'n_firing' in line: + cur_firing = line.split()[1] + expected_strikes[cur_firing] = [] + # Append index values of the current cell to 'n_firing' array. + else: + expected_strikes[cur_firing].append(int(line)) + + # # Development statements used to write comparison entries in expected_strikes + for n_firing in strikes.keys(): + # logging.info('n_firing ' + str(n_firing)) + # for i in range(len(strikes[n_firing]['strikes'])): + # if strikes[n_firing]['strikes'][i] > 0: + # # string = 'strikes[' + str(i) + '] = ' + str(strikes[n_firing]['cum_strikes'][i]) + # # logging.info(string) + + # logging.info(str(i)) + + # Sums the total amount of cell strikes per firing. Saves data to a dictionary. + # string = '\''+str(n_firing)+'\': ' + ' ' +str(strikes[n_firing]['strikes'].sum()) +',' + # logging.info(string) + + # Assert that each firing is striking the expected cells by comparing index values. + for i in range(len(strikes[n_firing]['strikes'])): + if strikes[n_firing]['strikes'][i] > 0: + self.assertIn(i, expected_strikes[n_firing]) + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod/rpod_integration_test_05.py b/tests/rpod/rpod_integration_test_05.py index 90ef4d8..f5fdd36 100644 --- a/tests/rpod/rpod_integration_test_05.py +++ b/tests/rpod/rpod_integration_test_05.py @@ -1,45 +1,45 @@ -# Juan P. Roldan -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_verification_test_03.py -# ======================== -# Test case for testing plume gas kinetic models in jfh firings **with multiple thrusters per firing**. - -import unittest, os, sys - -from pyrpod.vehicle import LogisticsModule, TargetVehicle, VisitingVehicle -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mission import MissionEnvironment - - -class LoadJFHChecks(unittest.TestCase): - def test_plume_constraints(self): - - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/rpod/multi_thrusters_square/' - - # Load JFH data. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - tv = TargetVehicle.TargetVehicle(case_dir) - tv.set_stl() - - vv = VisitingVehicle.VisitingVehicle(case_dir) - vv.set_stl() - vv.set_thruster_config() - vv.set_thruster_metrics() - - me = MissionEnvironment.MissionEnvironment(case_dir) - - plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) - plume_strike_study.study_init(jfh, tv, vv) - - plume_strike_study.graph_jfh() - plume_strike_study.jfh_plume_strikes() - -if __name__ == '__main__': +# Juan P. Roldan +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_verification_test_03.py +# ======================== +# Test case for testing plume gas kinetic models in jfh firings **with multiple thrusters per firing**. + +import unittest, os, sys + +from pyrpod.vehicle import LogisticsModule, TargetVehicle, VisitingVehicle +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mission import MissionEnvironment + + +class LoadJFHChecks(unittest.TestCase): + def test_plume_constraints(self): + + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/rpod/multi_thrusters_square/' + + # Load JFH data. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + tv = TargetVehicle.TargetVehicle(case_dir) + tv.set_stl() + + vv = VisitingVehicle.VisitingVehicle(case_dir) + vv.set_stl() + vv.set_thruster_config() + vv.set_thruster_metrics() + + me = MissionEnvironment.MissionEnvironment(case_dir) + + plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + plume_strike_study.study_init(jfh, tv, vv) + + plume_strike_study.graph_jfh() + plume_strike_study.jfh_plume_strikes() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod/rpod_unit_test_01.py b/tests/rpod/rpod_unit_test_01.py index 5881982..7e6f79f 100644 --- a/tests/rpod/rpod_unit_test_01.py +++ b/tests/rpod/rpod_unit_test_01.py @@ -1,75 +1,75 @@ -# Andy Torres, Pearce Patterson -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-28-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_unit_test_01.py -# ======================== -# Test case for converting STL data to VTK data. -# This is accomplished by checking for the proper data format of VTK files. -import unittest, os -import numpy as np -import meshio -from pyrpod.vehicle import Vehicle -from pyrpod.mission import MissionPlanner, MissionEnvironment - -class STLtoVTKChecks(unittest.TestCase): - def test_stl_to_vtk(self): - # 1. Setup - case_dir = '../case/rpod/stl_to_vtk/' - vtk_file_path = os.path.join(case_dir, 'results', 'cylinder.vtu') - - # Load mission planner and vehicle object data for analysis - me = MissionEnvironment.MissionEnvironment(case_dir) - mp = MissionPlanner.MissionPlanner(me) - v = Vehicle.Vehicle(case_dir) - - # 2. Execute - v.set_stl() # Read STL surface data - v.convert_stl_to_vtk() # Convert STL to VTK and save - - # Load cylinder STL and calculate cell and point counts - cylinder_stl = v.mesh - num_stl_cells = len(cylinder_stl.vectors) - num_stl_points = sum(map(len, [cylinder_stl.v2, cylinder_stl.v1, cylinder_stl.v0])) - - # Ensure STL mesh is non-empty - self.assertGreater(num_stl_cells, 0, "STL file contains no cells.") - self.assertGreater(num_stl_points, 0, "STL file contains no points.") - - # Load VTK file and calculate cell and point counts - self.assertTrue(os.path.exists(vtk_file_path), "VTK file was not created.") - cylinder_vtu = meshio.read(vtk_file_path) - num_vtu_points = len(cylinder_vtu.points) - num_vtu_cells = len(cylinder_vtu.cells_dict.get('triangle', [])) - - # Ensure VTK mesh is non-empty - self.assertGreater(num_vtu_cells, 0, "VTK file contains no cells.") - self.assertGreater(num_vtu_points, 0, "VTK file contains no points.") - - # 3. Assertions - # Assert cell and point counts match between STL and VTK - self.assertEqual(num_vtu_cells, num_stl_cells, "Cell counts do not match between STL and VTK.") - self.assertEqual(num_vtu_points, num_stl_points, "Point counts do not match between STL and VTK.") - - # Verify file extensions - # self.assertTrue(cylinder_stl.filename.endswith('.stl'), "Input file does not have .stl extension.") - self.assertTrue(vtk_file_path.endswith('.vtu'), "Output file does not have .vtu extension.") - - # Validate mesh bounds - stl_bounds = [np.min(cylinder_stl.vectors, axis=(0, 1)), np.max(cylinder_stl.vectors, axis=(0, 1))] - vtu_bounds = [np.min(cylinder_vtu.points, axis=0), np.max(cylinder_vtu.points, axis=0)] - self.assertTrue(np.allclose(stl_bounds, vtu_bounds, atol=1e-5), "Mesh bounds do not match between STL and VTK.") - - # Ensure VTK cells are of type 'triangle' - self.assertIn('triangle', cylinder_vtu.cells_dict, "VTK cells are not of type 'triangle'.") - - # Check surface normals consistency if available - if hasattr(cylinder_stl, 'normals') and 'Normals' in cylinder_vtu.point_data: - stl_normals = np.linalg.norm(cylinder_stl.normals, axis=1) - vtu_normals = np.linalg.norm(cylinder_vtu.point_data['Normals'], axis=1) - self.assertTrue(np.allclose(stl_normals, vtu_normals, atol=1e-5), "Surface normals do not match between STL and VTK.") - -if __name__ == '__main__': - unittest.main() +# Andy Torres, Pearce Patterson +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-28-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_unit_test_01.py +# ======================== +# Test case for converting STL data to VTK data. +# This is accomplished by checking for the proper data format of VTK files. +import unittest, os +import numpy as np +import meshio +from pyrpod.vehicle import Vehicle +from pyrpod.mission import MissionPlanner, MissionEnvironment + +class STLtoVTKChecks(unittest.TestCase): + def test_stl_to_vtk(self): + # 1. Setup + case_dir = '../case/rpod/stl_to_vtk/' + vtk_file_path = os.path.join(case_dir, 'results', 'cylinder.vtu') + + # Load mission planner and vehicle object data for analysis + me = MissionEnvironment.MissionEnvironment(case_dir) + mp = MissionPlanner.MissionPlanner(me) + v = Vehicle.Vehicle(case_dir) + + # 2. Execute + v.set_stl() # Read STL surface data + v.convert_stl_to_vtk() # Convert STL to VTK and save + + # Load cylinder STL and calculate cell and point counts + cylinder_stl = v.mesh + num_stl_cells = len(cylinder_stl.vectors) + num_stl_points = sum(map(len, [cylinder_stl.v2, cylinder_stl.v1, cylinder_stl.v0])) + + # Ensure STL mesh is non-empty + self.assertGreater(num_stl_cells, 0, "STL file contains no cells.") + self.assertGreater(num_stl_points, 0, "STL file contains no points.") + + # Load VTK file and calculate cell and point counts + self.assertTrue(os.path.exists(vtk_file_path), "VTK file was not created.") + cylinder_vtu = meshio.read(vtk_file_path) + num_vtu_points = len(cylinder_vtu.points) + num_vtu_cells = len(cylinder_vtu.cells_dict.get('triangle', [])) + + # Ensure VTK mesh is non-empty + self.assertGreater(num_vtu_cells, 0, "VTK file contains no cells.") + self.assertGreater(num_vtu_points, 0, "VTK file contains no points.") + + # 3. Assertions + # Assert cell and point counts match between STL and VTK + self.assertEqual(num_vtu_cells, num_stl_cells, "Cell counts do not match between STL and VTK.") + self.assertEqual(num_vtu_points, num_stl_points, "Point counts do not match between STL and VTK.") + + # Verify file extensions + # self.assertTrue(cylinder_stl.filename.endswith('.stl'), "Input file does not have .stl extension.") + self.assertTrue(vtk_file_path.endswith('.vtu'), "Output file does not have .vtu extension.") + + # Validate mesh bounds + stl_bounds = [np.min(cylinder_stl.vectors, axis=(0, 1)), np.max(cylinder_stl.vectors, axis=(0, 1))] + vtu_bounds = [np.min(cylinder_vtu.points, axis=0), np.max(cylinder_vtu.points, axis=0)] + self.assertTrue(np.allclose(stl_bounds, vtu_bounds, atol=1e-5), "Mesh bounds do not match between STL and VTK.") + + # Ensure VTK cells are of type 'triangle' + self.assertIn('triangle', cylinder_vtu.cells_dict, "VTK cells are not of type 'triangle'.") + + # Check surface normals consistency if available + if hasattr(cylinder_stl, 'normals') and 'Normals' in cylinder_vtu.point_data: + stl_normals = np.linalg.norm(cylinder_stl.normals, axis=1) + vtu_normals = np.linalg.norm(cylinder_vtu.point_data['Normals'], axis=1) + self.assertTrue(np.allclose(stl_normals, vtu_normals, atol=1e-5), "Surface normals do not match between STL and VTK.") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/rpod/rpod_unit_test_02.py b/tests/rpod/rpod_unit_test_02.py index 65ddfdf..518cea9 100644 --- a/tests/rpod/rpod_unit_test_02.py +++ b/tests/rpod/rpod_unit_test_02.py @@ -1,65 +1,65 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 03-16-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_unit_test_02.py -# ======================== -# Test case for reading in JFH data. - - -import unittest, os, sys -from pyrpod.rpod import JetFiringHistory - -def assert_dictionary_content(data): - for entry in data: - # Assert that all required keys exist - assert 'nt' in entry, "Key 'nt' is missing" - assert 'dt' in entry, "Key 'dt' is missing" - assert 't' in entry, "Key 't' is missing" - assert 'dcm' in entry, "Key 'dcm' is missing" - assert 'xyz' in entry, "Key 'xyz' is missing" - assert 'uf' in entry, "Key 'uf' is missing" - assert 'thrusters' in entry, "Key 'thrusters' is missing" - - # Assert the data types of values - assert isinstance(entry['nt'], str), "'nt' should be a string" - assert isinstance(entry['dt'], (float, str)), "'dt' should be a float or string" - assert isinstance(entry['t'], (float, str)), "'t' should be a float or string" - assert isinstance(entry['dcm'], list), "'dcm' should be a list" - assert len(entry['dcm']) == 3, "'dcm' should have 3 rows" - for row in entry['dcm']: - assert len(row) == 3, "Each row in 'dcm' should have 3 elements" - for value in row: - assert isinstance(value, float), "Values in 'dcm' should be floats" - - assert isinstance(entry['xyz'], list), "'xyz' should be a list" - assert len(entry['xyz']) == 3, "'xyz' should have 3 elements" - for value in entry['xyz']: - assert isinstance(value, float), "Values in 'xyz' should be floats" - - assert isinstance(entry['uf'], float), "'uf' should be a float" - - assert isinstance(entry['thrusters'], list), "'thrusters' should be a list" - for value in entry['thrusters']: - assert isinstance(value, int), "Values in 'thrusters' should be integers" - -class LoadJFHChecks(unittest.TestCase): - - def test_jfh_reader(self): - - # Path to directory holding data assets and results for a specific RPOD study. - case_dir = '../case/rpod/base_case/' - - # Load JFH data. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - data = [] - for firing in range(len(jfh.JFH)): - data.append(jfh.JFH[firing]) - - assert_dictionary_content(data) - -if __name__ == '__main__': +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 03-16-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_unit_test_02.py +# ======================== +# Test case for reading in JFH data. + + +import unittest, os, sys +from pyrpod.rpod import JetFiringHistory + +def assert_dictionary_content(data): + for entry in data: + # Assert that all required keys exist + assert 'nt' in entry, "Key 'nt' is missing" + assert 'dt' in entry, "Key 'dt' is missing" + assert 't' in entry, "Key 't' is missing" + assert 'dcm' in entry, "Key 'dcm' is missing" + assert 'xyz' in entry, "Key 'xyz' is missing" + assert 'uf' in entry, "Key 'uf' is missing" + assert 'thrusters' in entry, "Key 'thrusters' is missing" + + # Assert the data types of values + assert isinstance(entry['nt'], str), "'nt' should be a string" + assert isinstance(entry['dt'], (float, str)), "'dt' should be a float or string" + assert isinstance(entry['t'], (float, str)), "'t' should be a float or string" + assert isinstance(entry['dcm'], list), "'dcm' should be a list" + assert len(entry['dcm']) == 3, "'dcm' should have 3 rows" + for row in entry['dcm']: + assert len(row) == 3, "Each row in 'dcm' should have 3 elements" + for value in row: + assert isinstance(value, float), "Values in 'dcm' should be floats" + + assert isinstance(entry['xyz'], list), "'xyz' should be a list" + assert len(entry['xyz']) == 3, "'xyz' should have 3 elements" + for value in entry['xyz']: + assert isinstance(value, float), "Values in 'xyz' should be floats" + + assert isinstance(entry['uf'], float), "'uf' should be a float" + + assert isinstance(entry['thrusters'], list), "'thrusters' should be a list" + for value in entry['thrusters']: + assert isinstance(value, int), "Values in 'thrusters' should be integers" + +class LoadJFHChecks(unittest.TestCase): + + def test_jfh_reader(self): + + # Path to directory holding data assets and results for a specific RPOD study. + case_dir = '../case/rpod/base_case/' + + # Load JFH data. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + data = [] + for firing in range(len(jfh.JFH)): + data.append(jfh.JFH[firing]) + + assert_dictionary_content(data) + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod/rpod_unit_test_03.py b/tests/rpod/rpod_unit_test_03.py index 6206918..066745c 100644 --- a/tests/rpod/rpod_unit_test_03.py +++ b/tests/rpod/rpod_unit_test_03.py @@ -1,65 +1,65 @@ -# Andy Torres -# Embry-Riddle Aeronautical University -# Department of Aerospace Engineering -# Last Changed: 09-18-25 - -# ======================== -# PyRPOD: tests/rpod/rpod_unit_test_03.py -# ======================== -# Goal: Capture the exact file outputs of the three JFH printing helpers -# in pyrpod.util.io.file_print without making assertions yet. These files -# will serve as fixtures for future tests to lock current behavior. - -import unittest, os -import numpy as np - -from pyrpod.util.io import file_print as fp -from pyrpod.util.io.fs import ensure_dir - - -class CaptureJFHOutputs(unittest.TestCase): - def setUp(self): - # Output directory for captured files (kept within tests tree) - self.out_dir = os.path.join(os.path.dirname(__file__), 'jfh_outputs') - ensure_dir(self.out_dir) - - # Deterministic sample data used across all captures - # Three firings, simple linear times, simple positions, and rotation matrices - self.t_values = np.array([0.0, 1.5, 3.0]) - self.r = np.array([ - [1.2345, 2.3456, 3.4567], # x - [4.5678, 5.6789, 6.7890], # y - [7.8901, 8.9012, 9.0123], # z - ]) - - # For print_JFH and print_test_JFH, rot[i] is expected to have attribute .A - base_rot = np.array([ - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - ]) - self.rot_with_A = [np.array(base_rot) for _ in range(3)] - - # For print_1d_JFH, rot[i][j][k] indexing is used with scientific formatting - self.rot_array = [base_rot.copy() for _ in range(3)] - - def test_capture_print_JFH(self): - out_file = os.path.join(self.out_dir, 'print_JFH_output.txt') - fp.print_JFH(self.t_values, self.r, self.rot_with_A, out_file) - # No asserts yet; file is created as ground-truth capture - # Keep a tiny smoke check to ensure file write occurred successfully - self.assertTrue(os.path.exists(out_file)) - - def test_capture_print_test_JFH(self): - out_file = os.path.join(self.out_dir, 'print_test_JFH_output.txt') - fp.print_test_JFH(self.t_values, self.r, self.rot_with_A, out_file) - self.assertTrue(os.path.exists(out_file)) - - def test_capture_print_1d_JFH(self): - out_file = os.path.join(self.out_dir, 'print_1d_JFH_output.txt') - fp.print_1d_JFH(self.t_values, self.r, self.rot_array, out_file) - self.assertTrue(os.path.exists(out_file)) - - -if __name__ == '__main__': - unittest.main() +# Andy Torres +# Embry-Riddle Aeronautical University +# Department of Aerospace Engineering +# Last Changed: 09-18-25 + +# ======================== +# PyRPOD: tests/rpod/rpod_unit_test_03.py +# ======================== +# Goal: Capture the exact file outputs of the three JFH printing helpers +# in pyrpod.util.io.file_print without making assertions yet. These files +# will serve as fixtures for future tests to lock current behavior. + +import unittest, os +import numpy as np + +from pyrpod.util.io import file_print as fp +from pyrpod.util.io.fs import ensure_dir + + +class CaptureJFHOutputs(unittest.TestCase): + def setUp(self): + # Output directory for captured files (kept within tests tree) + self.out_dir = os.path.join(os.path.dirname(__file__), 'jfh_outputs') + ensure_dir(self.out_dir) + + # Deterministic sample data used across all captures + # Three firings, simple linear times, simple positions, and rotation matrices + self.t_values = np.array([0.0, 1.5, 3.0]) + self.r = np.array([ + [1.2345, 2.3456, 3.4567], # x + [4.5678, 5.6789, 6.7890], # y + [7.8901, 8.9012, 9.0123], # z + ]) + + # For print_JFH and print_test_JFH, rot[i] is expected to have attribute .A + base_rot = np.array([ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ]) + self.rot_with_A = [np.array(base_rot) for _ in range(3)] + + # For print_1d_JFH, rot[i][j][k] indexing is used with scientific formatting + self.rot_array = [base_rot.copy() for _ in range(3)] + + def test_capture_print_JFH(self): + out_file = os.path.join(self.out_dir, 'print_JFH_output.txt') + fp.print_JFH(self.t_values, self.r, self.rot_with_A, out_file) + # No asserts yet; file is created as ground-truth capture + # Keep a tiny smoke check to ensure file write occurred successfully + self.assertTrue(os.path.exists(out_file)) + + def test_capture_print_test_JFH(self): + out_file = os.path.join(self.out_dir, 'print_test_JFH_output.txt') + fp.print_test_JFH(self.t_values, self.r, self.rot_with_A, out_file) + self.assertTrue(os.path.exists(out_file)) + + def test_capture_print_1d_JFH(self): + out_file = os.path.join(self.out_dir, 'print_1d_JFH_output.txt') + fp.print_1d_JFH(self.t_values, self.r, self.rot_array, out_file) + self.assertTrue(os.path.exists(out_file)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/rpod/rpod_verification_test_04.py b/tests/rpod/rpod_verification_test_04.py index 7ad3a0a..0ae0aea 100644 --- a/tests/rpod/rpod_verification_test_04.py +++ b/tests/rpod/rpod_verification_test_04.py @@ -1,56 +1,56 @@ -# Nicholas A. Palumbo -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 3-21-24 - -# ======================== -# PyRPOD: tests/rpod_verification_test_04.py -# ======================== -# Visualizes STLs after decoupling the TCD and verifies that the plume strikes line up with the thrusters. - -import unittest, os, sys - -from pyrpod.vehicle import LogisticsModule, TargetVehicle, VisitingVehicle -from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy -from pyrpod.mission import MissionEnvironment - -class LoadSTLModels(unittest.TestCase): - def test_decoupled_tcd(self): - - # Set case directory. - case_dir = '../case/tcd_decoupling/' - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - # Instantiate TargetVehicle object. - tv = TargetVehicle.TargetVehicle(case_dir) - # Load Target Vehicle. - tv.set_stl() - - # Instantiate VisitingVehicle object. - vv = VisitingVehicle.VisitingVehicle(case_dir) - # Load Visiting Vehicle. - vv.set_stl() - # Load in thruster configuration file. - vv.set_thruster_config() - # Load in cluster configuration file. - vv.set_cluster_config() - # Load in thruster data file - vv.set_thruster_metrics() - - # Set mission environment. - me = MissionEnvironment.MissionEnvironment(case_dir) - - # Instantiate RPOD object. - plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) - # Initiate RPOD study. - plume_strike_study.study_init(jfh, tv, vv) - # Load STLs in Paraview - plume_strike_study.graph_jfh() - # Run plume strike analysis. - plume_strike_study.jfh_plume_strikes() - -if __name__ == '__main__': +# Nicholas A. Palumbo +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 3-21-24 + +# ======================== +# PyRPOD: tests/rpod_verification_test_04.py +# ======================== +# Visualizes STLs after decoupling the TCD and verifies that the plume strikes line up with the thrusters. + +import unittest, os, sys + +from pyrpod.vehicle import LogisticsModule, TargetVehicle, VisitingVehicle +from pyrpod.rpod import JetFiringHistory, PlumeStrikeEstimationStudy +from pyrpod.mission import MissionEnvironment + +class LoadSTLModels(unittest.TestCase): + def test_decoupled_tcd(self): + + # Set case directory. + case_dir = '../case/tcd_decoupling/' + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + # Instantiate TargetVehicle object. + tv = TargetVehicle.TargetVehicle(case_dir) + # Load Target Vehicle. + tv.set_stl() + + # Instantiate VisitingVehicle object. + vv = VisitingVehicle.VisitingVehicle(case_dir) + # Load Visiting Vehicle. + vv.set_stl() + # Load in thruster configuration file. + vv.set_thruster_config() + # Load in cluster configuration file. + vv.set_cluster_config() + # Load in thruster data file + vv.set_thruster_metrics() + + # Set mission environment. + me = MissionEnvironment.MissionEnvironment(case_dir) + + # Instantiate RPOD object. + plume_strike_study = PlumeStrikeEstimationStudy.PlumeStrikeEstimationStudy(me) + # Initiate RPOD study. + plume_strike_study.study_init(jfh, tv, vv) + # Load STLs in Paraview + plume_strike_study.graph_jfh() + # Run plume strike analysis. + plume_strike_study.jfh_plume_strikes() + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/rpod_verification_test_05.py b/tests/rpod_verification_test_05.py index 5a565f6..784a0de 100644 --- a/tests/rpod_verification_test_05.py +++ b/tests/rpod_verification_test_05.py @@ -1,54 +1,54 @@ -# Nicholas Palumbo -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 04-21-24 - -# ======================== -# PyRPOD: tests/rpod/rpod_verification_test_05.py -# ======================== -# Test case for visualizing the flow field using a series of TVs. - -import test_header -import unittest, os, sys -from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD - -class LoadPlume(unittest.TestCase): - def test_plume_field(self): - - # Set case directory. - case_dir = '../case/flow_visualization/' - - # Instantiate JetFiringHistory object. - jfh = JetFiringHistory.JetFiringHistory(case_dir) - jfh.read_jfh() - - # Instantiate TargetVehicle object. - tv = TargetVehicle.TargetVehicle(case_dir) - # Load Target Vehicle. - tv.set_stl() - - # Instantiate VisitingVehicle object. - vv = VisitingVehicle.VisitingVehicle(case_dir) - # Load Visiting Vehicle. - vv.set_stl() - # Load in thruster configuration file. - vv.set_thruster_config() - # Load in cluster configuration file. - vv.set_cluster_config() - # Load in thruster data file - vv.set_thruster_metrics() - - # Instantiate RPOD object. - rpod = RPOD.RPOD(case_dir) - # Initialize iteration counter to be used in graph_jfh and jfh_plume_strikes file naming - rpod.count = 1 - # Initiate RPOD study. - rpod.study_init(jfh, tv, vv) - # Load STLs in Paraview - rpod.graph_jfh() - # Run plume strike analysis. - rpod.jfh_plume_strikes() - - -if __name__ == '__main__': +# Nicholas Palumbo +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 04-21-24 + +# ======================== +# PyRPOD: tests/rpod/rpod_verification_test_05.py +# ======================== +# Test case for visualizing the flow field using a series of TVs. + +import test_header +import unittest, os, sys +from pyrpod import JetFiringHistory, TargetVehicle, VisitingVehicle, RPOD + +class LoadPlume(unittest.TestCase): + def test_plume_field(self): + + # Set case directory. + case_dir = '../case/flow_visualization/' + + # Instantiate JetFiringHistory object. + jfh = JetFiringHistory.JetFiringHistory(case_dir) + jfh.read_jfh() + + # Instantiate TargetVehicle object. + tv = TargetVehicle.TargetVehicle(case_dir) + # Load Target Vehicle. + tv.set_stl() + + # Instantiate VisitingVehicle object. + vv = VisitingVehicle.VisitingVehicle(case_dir) + # Load Visiting Vehicle. + vv.set_stl() + # Load in thruster configuration file. + vv.set_thruster_config() + # Load in cluster configuration file. + vv.set_cluster_config() + # Load in thruster data file + vv.set_thruster_metrics() + + # Instantiate RPOD object. + rpod = RPOD.RPOD(case_dir) + # Initialize iteration counter to be used in graph_jfh and jfh_plume_strikes file naming + rpod.count = 1 + # Initiate RPOD study. + rpod.study_init(jfh, tv, vv) + # Load STLs in Paraview + rpod.graph_jfh() + # Run plume strike analysis. + rpod.jfh_plume_strikes() + + +if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/validation/test_header.py b/validation/test_header.py index 3923546..59e5424 100644 --- a/validation/test_header.py +++ b/validation/test_header.py @@ -1,14 +1,14 @@ -# Andy Torres -# University of Central Florida -# Department of Mechanical and Aerospace Engineering -# Last Changed: 12-06-23 - -# ======================== -# PyRPOD: test/test_header.py -# ======================== -# Pretty janky, but importing this header file into your working directory -# is a simple way to allow the source code to be run outisde of it's own directory. - -import sys - +# Andy Torres +# University of Central Florida +# Department of Mechanical and Aerospace Engineering +# Last Changed: 12-06-23 + +# ======================== +# PyRPOD: test/test_header.py +# ======================== +# Pretty janky, but importing this header file into your working directory +# is a simple way to allow the source code to be run outisde of it's own directory. + +import sys + sys.path.insert(0, '../') \ No newline at end of file