From a27647fd86939ecb4b089a6157bbb3d30cdcdcaf Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 30 Aug 2017 10:29:50 +0200 Subject: [PATCH 01/36] Move matlab sources into subdir. --- basetestvec.m => matlab/basetestvec.m | 0 boundary_point.m => matlab/boundary_point.m | 0 .../bounding_boxes_intersection.m | 0 build_LS_matrix.m => matlab/build_LS_matrix.m | 0 build_reg_matrix.m => matlab/build_reg_matrix.m | 0 compute_control_points.m => matlab/compute_control_points.m | 0 compute_grid_lines.m => matlab/compute_grid_lines.m | 0 compute_patch_edges.m => matlab/compute_patch_edges.m | 0 find_int.m => matlab/find_int.m | 0 get_errors.m => matlab/get_errors.m | 0 get_knot_vector.m => matlab/get_knot_vector.m | 0 getpoints.m => matlab/getpoints.m | 0 getpoints2.m => matlab/getpoints2.m | 0 interpolate_intersections.m => matlab/interpolate_intersections.m | 0 intersections.m => matlab/intersections.m | 0 patch_patch_intersection.m => matlab/patch_patch_intersection.m | 0 plotresultsvec.m => matlab/plotresultsvec.m | 0 rangetest.m => matlab/rangetest.m | 0 solve.m => matlab/solve.m | 0 spline_surf_vec.m => matlab/spline_surf_vec.m | 0 splinebasevec.m => matlab/splinebasevec.m | 0 swaplines.m => matlab/swaplines.m | 0 transform.m => matlab/transform.m | 0 vec2mat.m => matlab/vec2mat.m | 0 24 files changed, 0 insertions(+), 0 deletions(-) rename basetestvec.m => matlab/basetestvec.m (100%) rename boundary_point.m => matlab/boundary_point.m (100%) rename bounding_boxes_intersection.m => matlab/bounding_boxes_intersection.m (100%) rename build_LS_matrix.m => matlab/build_LS_matrix.m (100%) rename build_reg_matrix.m => matlab/build_reg_matrix.m (100%) rename compute_control_points.m => matlab/compute_control_points.m (100%) rename compute_grid_lines.m => matlab/compute_grid_lines.m (100%) rename compute_patch_edges.m => matlab/compute_patch_edges.m (100%) rename find_int.m => matlab/find_int.m (100%) rename get_errors.m => matlab/get_errors.m (100%) rename get_knot_vector.m => matlab/get_knot_vector.m (100%) rename getpoints.m => matlab/getpoints.m (100%) rename getpoints2.m => matlab/getpoints2.m (100%) rename interpolate_intersections.m => matlab/interpolate_intersections.m (100%) rename intersections.m => matlab/intersections.m (100%) rename patch_patch_intersection.m => matlab/patch_patch_intersection.m (100%) rename plotresultsvec.m => matlab/plotresultsvec.m (100%) rename rangetest.m => matlab/rangetest.m (100%) rename solve.m => matlab/solve.m (100%) rename spline_surf_vec.m => matlab/spline_surf_vec.m (100%) rename splinebasevec.m => matlab/splinebasevec.m (100%) rename swaplines.m => matlab/swaplines.m (100%) rename transform.m => matlab/transform.m (100%) rename vec2mat.m => matlab/vec2mat.m (100%) diff --git a/basetestvec.m b/matlab/basetestvec.m similarity index 100% rename from basetestvec.m rename to matlab/basetestvec.m diff --git a/boundary_point.m b/matlab/boundary_point.m similarity index 100% rename from boundary_point.m rename to matlab/boundary_point.m diff --git a/bounding_boxes_intersection.m b/matlab/bounding_boxes_intersection.m similarity index 100% rename from bounding_boxes_intersection.m rename to matlab/bounding_boxes_intersection.m diff --git a/build_LS_matrix.m b/matlab/build_LS_matrix.m similarity index 100% rename from build_LS_matrix.m rename to matlab/build_LS_matrix.m diff --git a/build_reg_matrix.m b/matlab/build_reg_matrix.m similarity index 100% rename from build_reg_matrix.m rename to matlab/build_reg_matrix.m diff --git a/compute_control_points.m b/matlab/compute_control_points.m similarity index 100% rename from compute_control_points.m rename to matlab/compute_control_points.m diff --git a/compute_grid_lines.m b/matlab/compute_grid_lines.m similarity index 100% rename from compute_grid_lines.m rename to matlab/compute_grid_lines.m diff --git a/compute_patch_edges.m b/matlab/compute_patch_edges.m similarity index 100% rename from compute_patch_edges.m rename to matlab/compute_patch_edges.m diff --git a/find_int.m b/matlab/find_int.m similarity index 100% rename from find_int.m rename to matlab/find_int.m diff --git a/get_errors.m b/matlab/get_errors.m similarity index 100% rename from get_errors.m rename to matlab/get_errors.m diff --git a/get_knot_vector.m b/matlab/get_knot_vector.m similarity index 100% rename from get_knot_vector.m rename to matlab/get_knot_vector.m diff --git a/getpoints.m b/matlab/getpoints.m similarity index 100% rename from getpoints.m rename to matlab/getpoints.m diff --git a/getpoints2.m b/matlab/getpoints2.m similarity index 100% rename from getpoints2.m rename to matlab/getpoints2.m diff --git a/interpolate_intersections.m b/matlab/interpolate_intersections.m similarity index 100% rename from interpolate_intersections.m rename to matlab/interpolate_intersections.m diff --git a/intersections.m b/matlab/intersections.m similarity index 100% rename from intersections.m rename to matlab/intersections.m diff --git a/patch_patch_intersection.m b/matlab/patch_patch_intersection.m similarity index 100% rename from patch_patch_intersection.m rename to matlab/patch_patch_intersection.m diff --git a/plotresultsvec.m b/matlab/plotresultsvec.m similarity index 100% rename from plotresultsvec.m rename to matlab/plotresultsvec.m diff --git a/rangetest.m b/matlab/rangetest.m similarity index 100% rename from rangetest.m rename to matlab/rangetest.m diff --git a/solve.m b/matlab/solve.m similarity index 100% rename from solve.m rename to matlab/solve.m diff --git a/spline_surf_vec.m b/matlab/spline_surf_vec.m similarity index 100% rename from spline_surf_vec.m rename to matlab/spline_surf_vec.m diff --git a/splinebasevec.m b/matlab/splinebasevec.m similarity index 100% rename from splinebasevec.m rename to matlab/splinebasevec.m diff --git a/swaplines.m b/matlab/swaplines.m similarity index 100% rename from swaplines.m rename to matlab/swaplines.m diff --git a/transform.m b/matlab/transform.m similarity index 100% rename from transform.m rename to matlab/transform.m diff --git a/vec2mat.m b/matlab/vec2mat.m similarity index 100% rename from vec2mat.m rename to matlab/vec2mat.m From 88e9a79fa6f0bc82e3e9d36c548aae425fb6465a Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 30 Aug 2017 14:13:26 +0200 Subject: [PATCH 02/36] Populate with standrad files. --- .gitignore | 104 ++++++++ .travis.yml | 8 + LICENSE | 674 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 16 ++ requirements.txt | 1 + 5 files changed, 803 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5fabb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# PyCharm IDE +.idea/* + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0c94ad8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +nguage: python +python: + - "3.5" + - "3.6" +# command to install dependencies +install: "pip3 install -r requirements.txt" +# command to run tests +script: pytest diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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: + + {project} Copyright (C) {year} {fullname} + 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/README.md b/README.md new file mode 100644 index 0000000..a51ec23 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +Intersections +============== + +[![Build Status](https://travis-ci.org/GeoMop/Intersections.svg?branch=master)](https://travis-ci.org/GeoMop/Intersections) +[![Code Health](https://landscape.io/github/GeoMop/Intersections/master/landscape.svg?style=flat)](https://landscape.io/github/GeoMop/Intersections/master) +[![Code Climate](https://codeclimate.com/github/GeoMop/Intersections/badges/gpa.svg)](https://codeclimate.com/github/GeoMop/Intersections) +[![Test Coverage](https://codeclimate.com/github/GeoMop/Intersections/badges/coverage.svg)](https://codeclimate.com/github/GeoMop/Intersections/coverage) + + +Computing intersections of B-spline curves and surfaces. + +Library focus on fast intersection algorithms for non-degenerate intersections of B-spline curves and surfaces +of small degree (especially quadratic). + +Requirements +------------ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file From 1f92a21c5e9a9e0f6a69dfffad62b46b8dba638f Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 30 Aug 2017 14:14:42 +0200 Subject: [PATCH 03/36] Version of BIH-tree independent of Flow123d. --- src/BIH_tree/bih_node.hh | 141 +++++++++++++++ src/BIH_tree/bih_tree.cc | 276 ++++++++++++++++++++++++++++ src/BIH_tree/bih_tree.hh | 148 +++++++++++++++ src/BIH_tree/bounding_box.hh | 336 +++++++++++++++++++++++++++++++++++ src/BIH_tree/common.hh | 30 ++++ src/BIH_tree/makefile | 5 + 6 files changed, 936 insertions(+) create mode 100644 src/BIH_tree/bih_node.hh create mode 100644 src/BIH_tree/bih_tree.cc create mode 100644 src/BIH_tree/bih_tree.hh create mode 100644 src/BIH_tree/bounding_box.hh create mode 100644 src/BIH_tree/common.hh create mode 100644 src/BIH_tree/makefile diff --git a/src/BIH_tree/bih_node.hh b/src/BIH_tree/bih_node.hh new file mode 100644 index 0000000..f7ac1f7 --- /dev/null +++ b/src/BIH_tree/bih_node.hh @@ -0,0 +1,141 @@ +/*! + * + * Copyright (C) 2015 Technical University of Liberec. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License version 3 as published by the + * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) + * + * 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. + * + * + * @file bih_node.hh + * @brief + */ + +#ifndef BIH_NODE_HH_ +#define BIH_NODE_HH_ + +//#include "system/system.hh" +#include "bounding_box.hh" +//#include +#include + +class BIHNode { +public: + + /// count of subareas - don't change + static const unsigned int child_count = 2; + /// count of dimensions + static const unsigned char dimension = 3; + + /** + * Set leaf node. + */ + void set_leaf(unsigned int begin, unsigned int end, double bound,unsigned int depth) { + child_[0]=begin; + child_[1]=end; + bound_ = bound; + axis_=dimension+depth; + } + + /** + * Set non-leaf node. + */ + void set_non_leaf(unsigned int left, unsigned int right, unsigned int axis) { + ASSERT(axis < dimension," "); + ASSERT(is_leaf(), " "); // first must be leaf node (must set bound_) + axis_=axis; + child_[0]=left; + child_[1]=right; + } + + /// return true if node is leaf + bool is_leaf() const + { return axis_ >= dimension; } + + /// return depth of leaf node + + inline unsigned char depth() const + { + ASSERT( is_leaf(), "Not leaf node."); + return axis_ - dimension; + } + + unsigned int leaf_begin() const + { + ASSERT( is_leaf(), "Not leaf node."); + return child_[0]; + } + + unsigned int leaf_end() const + { + ASSERT( is_leaf(), "Not leaf node."); + return child_[1]; + } + + /** + * Get count of elements stored in + * + * @return Count of elements contained in node + */ + unsigned int leaf_size() const + { + ASSERT( is_leaf(), "Not leaf node."); + return child_[1] - child_[0]; + } + + /// return axes (coordination of splitting) of inner node + unsigned int axis() const + { + ASSERT(!is_leaf(), "Not in branch node.\n"); + return axis_; + } + + double bound() const + { + return bound_; + } + + /// Return index of child node. + unsigned int child(unsigned int i_child) const + { + ASSERT(!is_leaf(), "Not in branch node.\n"); + ASSERT( i_child < child_count, "" ); + return child_[i_child]; + + } +private: + + + /** + * Set depth of node to axes_ class members + * + * @param depth Depth of node in tree. + */ + void set_depth(unsigned int depth) { + axis_ = depth + dimension; + } + + /// child nodes indexes + unsigned int child_[child_count]; + + /** + * A non-leaf node has two childs (left and right). Their bounding boxes are created from the bounding box of parent + * so that in one direction the left child set max to its bound_ and the right child set its min to bound_. + * This way we can always repcreate bounding box of every node when traversing the tree from the root. + */ + double bound_; + + /** + * Value stores coordination of splitting area for inner nodes or depth for leaf nodes + * - values 0,1,2 indicate inner node of tree and coordination of splitting area + * - values 3 and greater indicate leaf node of tree and store depth of node (depth = axes_ - 3) + */ + unsigned char axis_; + +}; + +#endif /* BIH_NODE_HH_ */ diff --git a/src/BIH_tree/bih_tree.cc b/src/BIH_tree/bih_tree.cc new file mode 100644 index 0000000..925b79c --- /dev/null +++ b/src/BIH_tree/bih_tree.cc @@ -0,0 +1,276 @@ +/*! + * + * Copyright (C) 2015 Technical University of Liberec. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License version 3 as published by the + * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) + * + * 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. + * + * + * @file bih_tree.cc + * @brief + */ + +#include "bih_tree.hh" +#include "bih_node.hh" + +//#include +#include + +/** + * Minimum reduction of box size to allow + * splitting of a node during tree creation. + */ +const double BIHTree::size_reduce_factor = 0.8; + + +BIHTree::BIHTree(unsigned int soft_leaf_size_limit) +: leaf_size_limit(soft_leaf_size_limit), r_gen(123) +{ +} + + +BIHTree::~BIHTree() { +} + +void BIHTree::add_boxes(const std::vector &boxes) { + for(const BoundingBox & box : boxes) { + this->elements_.push_back(box); + main_box_.expand(box); + } +} + +void BIHTree::construct() { + ASSERT( elements_.size() > 0, " "); + + max_n_levels = 2*log(elements_.size())/log(2); + nodes_.reserve(2*elements_.size() / leaf_size_limit); + in_leaves_.resize(elements_.size()); + for(unsigned int i=0; i= median ) { + right_bound = std::min( right_bound, elements_[ *right ].min(axis) ); + --right; + } + std::swap( *left, *right); + } + } + // in any case left==right is now the first element of the right group + + if ( elements_[ *left ].projection_center(axis) < median) { + left_bound = std::max( left_bound, elements_[ *left ].max(axis) ); + ++left; + ++right; + } else { + right_bound = std::min( right_bound, elements_[ *right ].min(axis) ); + } + + unsigned int left_begin = node.leaf_begin(); + unsigned int left_end = left - in_leaves_.begin(); + unsigned int right_end = node.leaf_end(); + unsigned int depth = node.depth()+1; + // create new leaf nodes and possibly call split_node on them + // can not use node reference anymore + nodes_.push_back(BIHNode()); + nodes_.back().set_leaf(left_begin, left_end, left_bound, depth); + nodes_.push_back(BIHNode()); + nodes_.back().set_leaf(left_end, right_end, right_bound, depth); + + nodes_[node_idx].set_non_leaf(nodes_.size()-2, nodes_.size()-1, axis); + +// xprintf(Msg, "%d %f %f %f %f %d %d\n", node_idx, node_box.min(axis), left_bound, right_bound, node_box.max(axis), +// left_end - left_begin, right_end - left_end ); +} + + +void BIHTree::make_node(const BoundingBox &box, unsigned int node_idx) { + // we must refer to the node by index to prevent seg. fault due to nodes_ reallocation + + split_node(box,node_idx); + + { + BIHNode &node = nodes_[node_idx]; + BIHNode &child = nodes_[ node.child(0) ]; + BoundingBox node_box(box); + node_box.set_max(node.axis(), child.bound() ); + if ( child.leaf_size() > leaf_size_limit + && child.depth() < max_n_levels) +// && ( node.axis() != node_box.longest_axis() +// || node_box.size(node_box.longest_axis()) < box.size(node.axis()) * size_reduce_factor ) +// ) + { + make_node(node_box, node.child(0) ); + } +// else{ +// xprintf(Msg,"%d %d %f %f\n",node_idx, child.leaf_size(), +// node_box.size(node_box.longest_axis()), +// box.size(node.axis())); +// } + } + + { + BIHNode &node = nodes_[node_idx]; + BIHNode &child = nodes_[ node.child(1) ]; + BoundingBox node_box(box); + node_box.set_min(node.axis(), child.bound() ); + if ( child.leaf_size() > leaf_size_limit + && child.depth() < max_n_levels) +// && ( node.axis() != node_box.longest_axis() +// || node_box.size(node_box.longest_axis()) < box.size(node.axis()) * size_reduce_factor ) +// ) + { + make_node(node_box, node.child(1) ); + } +// else{ +// xprintf(Msg,"%d %d %f %f\n",node_idx, child.leaf_size(), +// node_box.size(node_box.longest_axis()), +// box.size(node.axis())); +// } + } +} + + +double BIHTree::estimate_median(unsigned char axis, const BIHNode &node) +{ + unsigned int median_idx; + unsigned int n_elements = node.leaf_size(); + + // TODO: possible optimizations: + // - try to apply nth_element directly to in_leaves_ array + // - if current approach is better (due to cache memory), check randomization of median for large meshes + // - good balancing of tree is crutial both for creation and find method + +// unsigned int sample_size = 50+n_elements/5; +// if (n_elements > sample_size) { +// // random sample +// std::uniform_int_distribution distribution(node.leaf_begin(), node.leaf_end()-1); +// coors_.resize(sample_size); +// for (unsigned int i=0; ir_gen); +// +// coors_[i] = elements_[ in_leaves_[ median_idx ] ].projection_center(axis); +// } +// +// } else + { + // all elements + coors_.resize(n_elements); + for (unsigned int i=0; i &result_list, bool full_list) const +{ + std::stack > node_stack; + ASSERT(result_list.size() == 0, ""); + + unsigned int counter = 0; + node_stack.push(0); + while (! node_stack.empty()) { + const BIHNode &node = nodes_[node_stack.top()]; + //DebugOut().fmt("node: {}\n", node_stack.top() ); + node_stack.pop(); + + + if (node.is_leaf()) { + + counter ++; + //START_TIMER("leaf"); + for (unsigned int i=node.leaf_begin(); i cpy(result_list); + sort(cpy.begin(), cpy.end()); + std::vector::iterator it = unique(cpy.begin(), cpy.end()); + OLD_ASSERT_EQUAL(cpy.size() , it - cpy.begin()); +#endif +} + + +void BIHTree::find_point(const Point &point, std::vector &result_list, bool full_list) const +{ + find_bounding_box(BoundingBox(point), result_list, full_list); +} + + + diff --git a/src/BIH_tree/bih_tree.hh b/src/BIH_tree/bih_tree.hh new file mode 100644 index 0000000..3d55fc5 --- /dev/null +++ b/src/BIH_tree/bih_tree.hh @@ -0,0 +1,148 @@ +/*! + * + * Copyright (C) 2015 Technical University of Liberec. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License version 3 as published by the + * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) + * + * 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. + * + * + * @file bih_tree.hh + * @brief + */ + +#ifndef BIH_TREE_HH_ +#define BIH_TREE_HH_ + +#include "bih_node.hh" +//#include "mesh/mesh.h" +//#include "mesh/point.hh" +//#include + + + +/** + * @brief Class for O(log N) lookup for intersections with a set of bounding boxes. + * + * Notes: + * Assumes spacedim=3. Implementation was designed for arbitrary number of childs per node, but + * currently it supports max 2 childs per node (binary tree). + * + */ +class BIHTree { +public: + /// count of dimensions + static const unsigned int dimension = 3; + /// max count of elements to estimate median - value must be even + static const unsigned int max_median_sample_size = 5; + + /** + * Constructor + * + * Set class members and call functions which create tree + * @param mesh - Mesh used for creation the tree + * @param soft_leaf_size_limit - Maximal number of elements stored in a leaf node of BIH tree. + */ + BIHTree(unsigned int soft_leaf_size_limit = 20); + + void add_boxes(const std::vector &boxes); + + void construct(); + + /** + * Destructor + */ + ~BIHTree(); + + /** + * Get count of elements stored in tree + * + * @return Count of bounding boxes stored in elements_ member + */ + unsigned int get_element_count() const; + + /** + * Main bounding box of the whole tree. + */ + const BoundingBox &tree_box() const; + + /** + * Gets elements which can have intersection with bounding box + * + * @param boundingBox Bounding box which is tested if has intersection + * @param result_list vector of ids of suspect elements + * @param full_list put to result_list all suspect elements found in leaf node or add only those that has intersection with boundingBox + */ + void find_bounding_box(const BoundingBox &boundingBox, std::vector &result_list, bool full_list = false) const; + + /** + * Gets elements which can have intersection with point + * + * @param point Point which is tested if has intersection + * @param result_list vector of ids of suspect elements + * @param full_list put to result_list all suspect elements found in leaf node or add only those that has intersection with point + */ + void find_point(const Point &point, std::vector &result_list, bool full_list = false) const; + + /** + * Get vector of mesh elements bounding boxes + * + * @return elements_ vector + */ + std::vector &get_elements() { return elements_; } + + /// Gets bounding box of element of given index @p ele_index. + const BoundingBox & ele_bounding_box(unsigned int ele_idx) const; + +protected: + /// required reduction in size of box to allow further splitting + static const double size_reduce_factor; + + /// create bounding boxes of element + //void element_boxes(); + + /// split tree node given by node_idx, distribute elements to child nodes + void split_node(const BoundingBox &node_box, unsigned int node_idx); + + /// create child nodes of node given by node_idx + void make_node(const BoundingBox &box, unsigned int node_idx); + + /** + * For given node takes projection of centers of bounding boxes of its elements to axis given by + * @p node::axis() + * and estimate median of these values. That is optimal split point. + * Precise median is computed for sets smaller then @p max_median_sample_size + * estimate from random sample is used for larger sets. + */ + double estimate_median(unsigned char axis, const BIHNode &node); + + /// mesh + //Mesh* mesh_; + /// vector of mesh elements bounding boxes (from mesh) + std::vector elements_; + /// Main bounding box. (from mesh) + BoundingBox main_box_; + + /// vector of tree nodes + std::vector nodes_; + /// Maximal number of elements stored in a leaf node of BIH tree. + unsigned int leaf_size_limit; + /// Maximal count of BIH tree levels + unsigned int max_n_levels; + + /// vector stored element indexes in leaf nodes + std::vector in_leaves_; + /// temporary vector stored values of coordinations for calculating median + std::vector coors_; + + // random generator + std::mt19937 r_gen; + + +}; + +#endif /* BIH_TREE_HH_ */ diff --git a/src/BIH_tree/bounding_box.hh b/src/BIH_tree/bounding_box.hh new file mode 100644 index 0000000..afb3947 --- /dev/null +++ b/src/BIH_tree/bounding_box.hh @@ -0,0 +1,336 @@ +/*! + * + * Copyright (C) 2015 Technical University of Liberec. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License version 3 as published by the + * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) + * + * 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. + * + * + * @file bounding_box.hh + * @brief + */ + +#ifndef BOX_ELEMENT_HH_ +#define BOX_ELEMENT_HH_ + + +#include +//#include +#include + +#include "common.hh" +#include +#include + +using namespace std; + +struct Point +{ + std::array p_; + + Point() : + p_({0,0,0}) + {} + + Point(double x, double y, double z) : + p_({x,y,z}) + {} + + double &operator [](uint i) { + return p_[i]; + } + + const double &operator [](uint i) const { + return p_[i]; + } + + +}; + +Point operator+(Point a, Point b) { + return Point( a[0]+b[0], a[1]+b[1], a[2]+b[2]); +} + +Point operator-(Point a, Point b) { + return Point( a[0]-b[0], a[1]-b[1], a[2]-b[2]); +} + +Point operator*(double a, Point b) { + return Point( a*b[0], a*b[1], a*b[2]); +} + + +bool operator <=(Point a, Point b) { + return + a[0] <= b[0] && + a[1] <= b[1] && + a[2] <= b[2]; +} + +/** + * @brief Bounding box in 3d ambient space. + * + * Primary intention is usage in BIHTree and various speedups of non-compatible + * intersections. + * + * Copy constructor and assignment are default provided by compiler. + * These can be used to set bounds latter on without particular method + * to this end: + * + * @code + * BoundingBox box; // non-initialized box + * box=BoundingBox( arma::vec3("0 1 2"), arma::vec3("4 5 6") ); + * @endcode + * + * Don;t worry about performance, all is inlined. + */ +class BoundingBox { +public: + + /// Currently we set dimension to 3. + static const unsigned int dimension = 3; + /// stabilization parameter + static constexpr double epsilon = 64 * std::numeric_limits::epsilon(); + + + + /** + * Default constructor. + * No initialization of vertices. Be very careful using this. + * One necessary usage is vector of BoundigBox. + */ + BoundingBox() {} + + /** + * Constructor for point box. + */ + BoundingBox(const Point &min) + : min_vertex_(min), max_vertex_(min) + {}; + + /** + * Constructor. + * + * From given minimal and maximal vertex. + */ + BoundingBox(const Point &min, const Point &max) + : min_vertex_(min), max_vertex_(max) + { + ASSERT( min <= max , "Wrong coordinates in constructor."); + }; + + /** + * Constructor. + * + * Make bounding box for set of points. + */ + BoundingBox(const vector &points) { + ASSERT( 0 < points.size(), "" ); + + auto it = points.begin(); + max_vertex_ = min_vertex_ = *it; + ++it; + for(; it != points.end(); ++it) expand( *it ); + } + + + /** + * Set maximum in given axis. + */ + void set_max(unsigned int axis, double max) { + ASSERT(axis < dimension, ""); + ASSERT( min(axis) <= max, ""); + max_vertex_[axis] = max; + } + + /** + * Set minimum on given axis. + */ + void set_min(unsigned int axis, double min) { + ASSERT(axis < dimension, ""); + ASSERT(min <= max(axis), ""); + min_vertex_[axis] = min; + } + + /** + * Return minimal vertex of the bounding box. + */ + const Point &min() const { + return min_vertex_; + } + + /** + * Return maximal vertex of the bounding box. + */ + const Point &max() const { + return max_vertex_; + } + + /** + * Return minimal value on given axis. + */ + double min(unsigned int axis) const { + return min()[axis]; + } + + /** + * Return maximal value on given axis. + */ + double max(unsigned int axis) const { + return max()[axis]; + } + + + /** + * Return size of the box in given axis. + */ + double size(unsigned int axis) const { + return max()[axis] - min()[axis]; + } + + + /** + * Return center of the bounding box. + */ + Point center() const { + return 0.5 * (max_vertex_ + min_vertex_); + } + + /** + * Return center of projection of the bounding box to given @p axis. + * Axis coding is: 0 - axis x, 1 - axis y, 2 - axis z. + */ + double projection_center(unsigned int axis) const{ + ASSERT(axis < dimension, ""); + return 0.5 * (max_vertex_[axis] + min_vertex_[axis]); + } + + /** + * Returns true is the box element contains @p point + * + * @param point Testing point + * @return True if box element contains point + */ + bool contains_point(const Point &point) const + { + for (unsigned int i=0; i b2.max_vertex_[i] + epsilon) || + (b2.min_vertex_[i] > max_vertex_[i] + epsilon ) ) return false; + } + return true; + } + + /** + * Returns true if projection of the box to @p axis is an interval + * less then (with tolerance) to given @p value. + */ + bool projection_lt(unsigned int axis, double value) const + { + return max_vertex_[axis] + epsilon < value; + } + + /** + * Returns true if projection of the box to @p axis is an interval + * greater then (with tolerance) to given @p value. + */ + bool projection_gt(unsigned int axis, double value) const + { + return min_vertex_[axis] - epsilon > value; + } + + /** + * Split box into two boxes along @p axis by the + * plane going through @p splitting_point on the axis. + */ + void split(unsigned int axis, double splitting_point, + BoundingBox &left, BoundingBox &right ) const + { + ASSERT(axis < dimension, ""); + if (min_vertex_[axis] <= splitting_point && splitting_point <= max_vertex_[axis] ) { + left = *this; + right = *this; + left.max_vertex_[axis] = splitting_point; + right.min_vertex_[axis] = splitting_point; + } else { + throw; + //THROW( ExcSplitting() << EI_interval_left(min_vertex_[axis]) + // << EI_interval_right(max_vertex_[axis]) + // << EI_split_point(splitting_point) ); + } + } + + /** + * Expand bounding box to contain also given @p point. + */ + void expand(const Point &point) { + for(unsigned int j=0; j diff[0]) + ? ( diff[2] > diff[1] ? 2 : 1 ) + : ( diff[2] > diff[0] ? 2 : 0 ); + } + + +private: + /// minimal coordinates of bounding box + Point min_vertex_; + /// maximal coordinates of bounding box + Point max_vertex_; +}; + +/// Overloads output operator for box. +inline ostream &operator<<(ostream &stream, const BoundingBox &box) { + stream << "Box(" + << box.min(0) << " " + << box.min(1) << " " + << box.min(2) << "; " + << box.max(0) << " " + << box.max(1) << " " + << box.max(2) << ")"; + return stream; +} + +#endif /* BOX_ELEMENT_HH_ */ diff --git a/src/BIH_tree/common.hh b/src/BIH_tree/common.hh new file mode 100644 index 0000000..f02814a --- /dev/null +++ b/src/BIH_tree/common.hh @@ -0,0 +1,30 @@ +/*! + * + * Copyright (C) 2015 Technical University of Liberec. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License version 3 as published by the + * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) + * + * 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. + * + * + * @file bounding_box.hh + * @brief + */ + +#ifndef COMMON_HH_ +#define COMMON_HH_ + +#if DEBUG +#define ASSERT( COND, MESSAGE) + if (! (COND)) { + cout << MESSAGE; + } +#else +#define ASSERT(COND, MESSAGE) +#endif + +#endif diff --git a/src/BIH_tree/makefile b/src/BIH_tree/makefile new file mode 100644 index 0000000..4b67e22 --- /dev/null +++ b/src/BIH_tree/makefile @@ -0,0 +1,5 @@ +all: + $(CC) -std=c++11 -c bih_tree.cc + +clean: + rm *.o \ No newline at end of file From 50db4e981c15babe946323bcce23ea4dbeb23d10 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 30 Aug 2017 14:15:29 +0200 Subject: [PATCH 04/36] Basic Python files. --- src/bs_curve.py | 12 ++++++++++++ src/bs_surface.py | 5 +++++ src/isec_curv_surf.py | 8 ++++++++ src/isec_surf_surf.py | 8 ++++++++ 4 files changed, 33 insertions(+) create mode 100644 src/bs_curve.py create mode 100644 src/bs_surface.py create mode 100644 src/isec_curv_surf.py create mode 100644 src/isec_surf_surf.py diff --git a/src/bs_curve.py b/src/bs_curve.py new file mode 100644 index 0000000..7a38ed0 --- /dev/null +++ b/src/bs_curve.py @@ -0,0 +1,12 @@ +class BSCurve: + ''' + Class representing a general B-spline curve. + Specializations for particular degree are derived. + ''' + pass + + +class BSCurveD2: + ''' + Quadratic B-spline. + ''' \ No newline at end of file diff --git a/src/bs_surface.py b/src/bs_surface.py new file mode 100644 index 0000000..c10a266 --- /dev/null +++ b/src/bs_surface.py @@ -0,0 +1,5 @@ +class BSSurface: + ''' + Representation of the B-spline surface. + ''' + pass \ No newline at end of file diff --git a/src/isec_curv_surf.py b/src/isec_curv_surf.py new file mode 100644 index 0000000..980ba0c --- /dev/null +++ b/src/isec_curv_surf.py @@ -0,0 +1,8 @@ +class IsecCurvSurf: + ''' + Class for calculation and representation of the intersection of + B-spline curve and B-spline surface. + Currently only (multi)-point intersections are assumed. + ''' + def __init__(self, curve, surface): + pass \ No newline at end of file diff --git a/src/isec_surf_surf.py b/src/isec_surf_surf.py new file mode 100644 index 0000000..4ff7684 --- /dev/null +++ b/src/isec_surf_surf.py @@ -0,0 +1,8 @@ +class IsecSurfSurf: + ''' + Calculation and representation of intersection of two B-spline surfaces. + + Result is set of B-spline segments approximating the intersection branches. + Every segment consists of: a 3D curve and two 2D curves in parametric UV space of the surfaces. + ''' + \ No newline at end of file From 5c891f433c5ec50890e251eb768dbf13b4aae1b1 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 30 Aug 2017 14:28:27 +0200 Subject: [PATCH 05/36] Add pybind11 as submodule. --- .gitmodules | 4 ++++ external/pybind11 | 1 + 2 files changed, 5 insertions(+) create mode 100644 .gitmodules create mode 160000 external/pybind11 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..53c39bf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "external/pybind11"] + path = external/pybind11 + url = git@github.com:pybind/pybind11.git + branch = stable diff --git a/external/pybind11 b/external/pybind11 new file mode 160000 index 0000000..1df91d3 --- /dev/null +++ b/external/pybind11 @@ -0,0 +1 @@ +Subproject commit 1df91d36cfa997c8987889234304ea5e37cc3e68 From ee3ad4c46450b31975d563133b4beae0e4d61af9 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 31 Aug 2017 19:28:25 +0200 Subject: [PATCH 06/36] Remove PYBIND --- .gitmodules | 4 ---- external/pybind11 | 1 - 2 files changed, 5 deletions(-) delete mode 160000 external/pybind11 diff --git a/.gitmodules b/.gitmodules index 53c39bf..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "external/pybind11"] - path = external/pybind11 - url = git@github.com:pybind/pybind11.git - branch = stable diff --git a/external/pybind11 b/external/pybind11 deleted file mode 160000 index 1df91d3..0000000 --- a/external/pybind11 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1df91d36cfa997c8987889234304ea5e37cc3e68 From d54592db6c2ff746f5b904deed0aab2dc1573f2d Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 31 Aug 2017 19:29:37 +0200 Subject: [PATCH 07/36] Remove BIH sources. --- src/BIH_tree/bih_node.hh | 141 --------------- src/BIH_tree/bih_tree.cc | 276 ---------------------------- src/BIH_tree/bih_tree.hh | 148 --------------- src/BIH_tree/bounding_box.hh | 336 ----------------------------------- src/BIH_tree/common.hh | 30 ---- src/BIH_tree/makefile | 5 - 6 files changed, 936 deletions(-) delete mode 100644 src/BIH_tree/bih_node.hh delete mode 100644 src/BIH_tree/bih_tree.cc delete mode 100644 src/BIH_tree/bih_tree.hh delete mode 100644 src/BIH_tree/bounding_box.hh delete mode 100644 src/BIH_tree/common.hh delete mode 100644 src/BIH_tree/makefile diff --git a/src/BIH_tree/bih_node.hh b/src/BIH_tree/bih_node.hh deleted file mode 100644 index f7ac1f7..0000000 --- a/src/BIH_tree/bih_node.hh +++ /dev/null @@ -1,141 +0,0 @@ -/*! - * - * Copyright (C) 2015 Technical University of Liberec. All rights reserved. - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License version 3 as published by the - * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) - * - * 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. - * - * - * @file bih_node.hh - * @brief - */ - -#ifndef BIH_NODE_HH_ -#define BIH_NODE_HH_ - -//#include "system/system.hh" -#include "bounding_box.hh" -//#include -#include - -class BIHNode { -public: - - /// count of subareas - don't change - static const unsigned int child_count = 2; - /// count of dimensions - static const unsigned char dimension = 3; - - /** - * Set leaf node. - */ - void set_leaf(unsigned int begin, unsigned int end, double bound,unsigned int depth) { - child_[0]=begin; - child_[1]=end; - bound_ = bound; - axis_=dimension+depth; - } - - /** - * Set non-leaf node. - */ - void set_non_leaf(unsigned int left, unsigned int right, unsigned int axis) { - ASSERT(axis < dimension," "); - ASSERT(is_leaf(), " "); // first must be leaf node (must set bound_) - axis_=axis; - child_[0]=left; - child_[1]=right; - } - - /// return true if node is leaf - bool is_leaf() const - { return axis_ >= dimension; } - - /// return depth of leaf node - - inline unsigned char depth() const - { - ASSERT( is_leaf(), "Not leaf node."); - return axis_ - dimension; - } - - unsigned int leaf_begin() const - { - ASSERT( is_leaf(), "Not leaf node."); - return child_[0]; - } - - unsigned int leaf_end() const - { - ASSERT( is_leaf(), "Not leaf node."); - return child_[1]; - } - - /** - * Get count of elements stored in - * - * @return Count of elements contained in node - */ - unsigned int leaf_size() const - { - ASSERT( is_leaf(), "Not leaf node."); - return child_[1] - child_[0]; - } - - /// return axes (coordination of splitting) of inner node - unsigned int axis() const - { - ASSERT(!is_leaf(), "Not in branch node.\n"); - return axis_; - } - - double bound() const - { - return bound_; - } - - /// Return index of child node. - unsigned int child(unsigned int i_child) const - { - ASSERT(!is_leaf(), "Not in branch node.\n"); - ASSERT( i_child < child_count, "" ); - return child_[i_child]; - - } -private: - - - /** - * Set depth of node to axes_ class members - * - * @param depth Depth of node in tree. - */ - void set_depth(unsigned int depth) { - axis_ = depth + dimension; - } - - /// child nodes indexes - unsigned int child_[child_count]; - - /** - * A non-leaf node has two childs (left and right). Their bounding boxes are created from the bounding box of parent - * so that in one direction the left child set max to its bound_ and the right child set its min to bound_. - * This way we can always repcreate bounding box of every node when traversing the tree from the root. - */ - double bound_; - - /** - * Value stores coordination of splitting area for inner nodes or depth for leaf nodes - * - values 0,1,2 indicate inner node of tree and coordination of splitting area - * - values 3 and greater indicate leaf node of tree and store depth of node (depth = axes_ - 3) - */ - unsigned char axis_; - -}; - -#endif /* BIH_NODE_HH_ */ diff --git a/src/BIH_tree/bih_tree.cc b/src/BIH_tree/bih_tree.cc deleted file mode 100644 index 925b79c..0000000 --- a/src/BIH_tree/bih_tree.cc +++ /dev/null @@ -1,276 +0,0 @@ -/*! - * - * Copyright (C) 2015 Technical University of Liberec. All rights reserved. - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License version 3 as published by the - * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) - * - * 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. - * - * - * @file bih_tree.cc - * @brief - */ - -#include "bih_tree.hh" -#include "bih_node.hh" - -//#include -#include - -/** - * Minimum reduction of box size to allow - * splitting of a node during tree creation. - */ -const double BIHTree::size_reduce_factor = 0.8; - - -BIHTree::BIHTree(unsigned int soft_leaf_size_limit) -: leaf_size_limit(soft_leaf_size_limit), r_gen(123) -{ -} - - -BIHTree::~BIHTree() { -} - -void BIHTree::add_boxes(const std::vector &boxes) { - for(const BoundingBox & box : boxes) { - this->elements_.push_back(box); - main_box_.expand(box); - } -} - -void BIHTree::construct() { - ASSERT( elements_.size() > 0, " "); - - max_n_levels = 2*log(elements_.size())/log(2); - nodes_.reserve(2*elements_.size() / leaf_size_limit); - in_leaves_.resize(elements_.size()); - for(unsigned int i=0; i= median ) { - right_bound = std::min( right_bound, elements_[ *right ].min(axis) ); - --right; - } - std::swap( *left, *right); - } - } - // in any case left==right is now the first element of the right group - - if ( elements_[ *left ].projection_center(axis) < median) { - left_bound = std::max( left_bound, elements_[ *left ].max(axis) ); - ++left; - ++right; - } else { - right_bound = std::min( right_bound, elements_[ *right ].min(axis) ); - } - - unsigned int left_begin = node.leaf_begin(); - unsigned int left_end = left - in_leaves_.begin(); - unsigned int right_end = node.leaf_end(); - unsigned int depth = node.depth()+1; - // create new leaf nodes and possibly call split_node on them - // can not use node reference anymore - nodes_.push_back(BIHNode()); - nodes_.back().set_leaf(left_begin, left_end, left_bound, depth); - nodes_.push_back(BIHNode()); - nodes_.back().set_leaf(left_end, right_end, right_bound, depth); - - nodes_[node_idx].set_non_leaf(nodes_.size()-2, nodes_.size()-1, axis); - -// xprintf(Msg, "%d %f %f %f %f %d %d\n", node_idx, node_box.min(axis), left_bound, right_bound, node_box.max(axis), -// left_end - left_begin, right_end - left_end ); -} - - -void BIHTree::make_node(const BoundingBox &box, unsigned int node_idx) { - // we must refer to the node by index to prevent seg. fault due to nodes_ reallocation - - split_node(box,node_idx); - - { - BIHNode &node = nodes_[node_idx]; - BIHNode &child = nodes_[ node.child(0) ]; - BoundingBox node_box(box); - node_box.set_max(node.axis(), child.bound() ); - if ( child.leaf_size() > leaf_size_limit - && child.depth() < max_n_levels) -// && ( node.axis() != node_box.longest_axis() -// || node_box.size(node_box.longest_axis()) < box.size(node.axis()) * size_reduce_factor ) -// ) - { - make_node(node_box, node.child(0) ); - } -// else{ -// xprintf(Msg,"%d %d %f %f\n",node_idx, child.leaf_size(), -// node_box.size(node_box.longest_axis()), -// box.size(node.axis())); -// } - } - - { - BIHNode &node = nodes_[node_idx]; - BIHNode &child = nodes_[ node.child(1) ]; - BoundingBox node_box(box); - node_box.set_min(node.axis(), child.bound() ); - if ( child.leaf_size() > leaf_size_limit - && child.depth() < max_n_levels) -// && ( node.axis() != node_box.longest_axis() -// || node_box.size(node_box.longest_axis()) < box.size(node.axis()) * size_reduce_factor ) -// ) - { - make_node(node_box, node.child(1) ); - } -// else{ -// xprintf(Msg,"%d %d %f %f\n",node_idx, child.leaf_size(), -// node_box.size(node_box.longest_axis()), -// box.size(node.axis())); -// } - } -} - - -double BIHTree::estimate_median(unsigned char axis, const BIHNode &node) -{ - unsigned int median_idx; - unsigned int n_elements = node.leaf_size(); - - // TODO: possible optimizations: - // - try to apply nth_element directly to in_leaves_ array - // - if current approach is better (due to cache memory), check randomization of median for large meshes - // - good balancing of tree is crutial both for creation and find method - -// unsigned int sample_size = 50+n_elements/5; -// if (n_elements > sample_size) { -// // random sample -// std::uniform_int_distribution distribution(node.leaf_begin(), node.leaf_end()-1); -// coors_.resize(sample_size); -// for (unsigned int i=0; ir_gen); -// -// coors_[i] = elements_[ in_leaves_[ median_idx ] ].projection_center(axis); -// } -// -// } else - { - // all elements - coors_.resize(n_elements); - for (unsigned int i=0; i &result_list, bool full_list) const -{ - std::stack > node_stack; - ASSERT(result_list.size() == 0, ""); - - unsigned int counter = 0; - node_stack.push(0); - while (! node_stack.empty()) { - const BIHNode &node = nodes_[node_stack.top()]; - //DebugOut().fmt("node: {}\n", node_stack.top() ); - node_stack.pop(); - - - if (node.is_leaf()) { - - counter ++; - //START_TIMER("leaf"); - for (unsigned int i=node.leaf_begin(); i cpy(result_list); - sort(cpy.begin(), cpy.end()); - std::vector::iterator it = unique(cpy.begin(), cpy.end()); - OLD_ASSERT_EQUAL(cpy.size() , it - cpy.begin()); -#endif -} - - -void BIHTree::find_point(const Point &point, std::vector &result_list, bool full_list) const -{ - find_bounding_box(BoundingBox(point), result_list, full_list); -} - - - diff --git a/src/BIH_tree/bih_tree.hh b/src/BIH_tree/bih_tree.hh deleted file mode 100644 index 3d55fc5..0000000 --- a/src/BIH_tree/bih_tree.hh +++ /dev/null @@ -1,148 +0,0 @@ -/*! - * - * Copyright (C) 2015 Technical University of Liberec. All rights reserved. - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License version 3 as published by the - * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) - * - * 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. - * - * - * @file bih_tree.hh - * @brief - */ - -#ifndef BIH_TREE_HH_ -#define BIH_TREE_HH_ - -#include "bih_node.hh" -//#include "mesh/mesh.h" -//#include "mesh/point.hh" -//#include - - - -/** - * @brief Class for O(log N) lookup for intersections with a set of bounding boxes. - * - * Notes: - * Assumes spacedim=3. Implementation was designed for arbitrary number of childs per node, but - * currently it supports max 2 childs per node (binary tree). - * - */ -class BIHTree { -public: - /// count of dimensions - static const unsigned int dimension = 3; - /// max count of elements to estimate median - value must be even - static const unsigned int max_median_sample_size = 5; - - /** - * Constructor - * - * Set class members and call functions which create tree - * @param mesh - Mesh used for creation the tree - * @param soft_leaf_size_limit - Maximal number of elements stored in a leaf node of BIH tree. - */ - BIHTree(unsigned int soft_leaf_size_limit = 20); - - void add_boxes(const std::vector &boxes); - - void construct(); - - /** - * Destructor - */ - ~BIHTree(); - - /** - * Get count of elements stored in tree - * - * @return Count of bounding boxes stored in elements_ member - */ - unsigned int get_element_count() const; - - /** - * Main bounding box of the whole tree. - */ - const BoundingBox &tree_box() const; - - /** - * Gets elements which can have intersection with bounding box - * - * @param boundingBox Bounding box which is tested if has intersection - * @param result_list vector of ids of suspect elements - * @param full_list put to result_list all suspect elements found in leaf node or add only those that has intersection with boundingBox - */ - void find_bounding_box(const BoundingBox &boundingBox, std::vector &result_list, bool full_list = false) const; - - /** - * Gets elements which can have intersection with point - * - * @param point Point which is tested if has intersection - * @param result_list vector of ids of suspect elements - * @param full_list put to result_list all suspect elements found in leaf node or add only those that has intersection with point - */ - void find_point(const Point &point, std::vector &result_list, bool full_list = false) const; - - /** - * Get vector of mesh elements bounding boxes - * - * @return elements_ vector - */ - std::vector &get_elements() { return elements_; } - - /// Gets bounding box of element of given index @p ele_index. - const BoundingBox & ele_bounding_box(unsigned int ele_idx) const; - -protected: - /// required reduction in size of box to allow further splitting - static const double size_reduce_factor; - - /// create bounding boxes of element - //void element_boxes(); - - /// split tree node given by node_idx, distribute elements to child nodes - void split_node(const BoundingBox &node_box, unsigned int node_idx); - - /// create child nodes of node given by node_idx - void make_node(const BoundingBox &box, unsigned int node_idx); - - /** - * For given node takes projection of centers of bounding boxes of its elements to axis given by - * @p node::axis() - * and estimate median of these values. That is optimal split point. - * Precise median is computed for sets smaller then @p max_median_sample_size - * estimate from random sample is used for larger sets. - */ - double estimate_median(unsigned char axis, const BIHNode &node); - - /// mesh - //Mesh* mesh_; - /// vector of mesh elements bounding boxes (from mesh) - std::vector elements_; - /// Main bounding box. (from mesh) - BoundingBox main_box_; - - /// vector of tree nodes - std::vector nodes_; - /// Maximal number of elements stored in a leaf node of BIH tree. - unsigned int leaf_size_limit; - /// Maximal count of BIH tree levels - unsigned int max_n_levels; - - /// vector stored element indexes in leaf nodes - std::vector in_leaves_; - /// temporary vector stored values of coordinations for calculating median - std::vector coors_; - - // random generator - std::mt19937 r_gen; - - -}; - -#endif /* BIH_TREE_HH_ */ diff --git a/src/BIH_tree/bounding_box.hh b/src/BIH_tree/bounding_box.hh deleted file mode 100644 index afb3947..0000000 --- a/src/BIH_tree/bounding_box.hh +++ /dev/null @@ -1,336 +0,0 @@ -/*! - * - * Copyright (C) 2015 Technical University of Liberec. All rights reserved. - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License version 3 as published by the - * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) - * - * 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. - * - * - * @file bounding_box.hh - * @brief - */ - -#ifndef BOX_ELEMENT_HH_ -#define BOX_ELEMENT_HH_ - - -#include -//#include -#include - -#include "common.hh" -#include -#include - -using namespace std; - -struct Point -{ - std::array p_; - - Point() : - p_({0,0,0}) - {} - - Point(double x, double y, double z) : - p_({x,y,z}) - {} - - double &operator [](uint i) { - return p_[i]; - } - - const double &operator [](uint i) const { - return p_[i]; - } - - -}; - -Point operator+(Point a, Point b) { - return Point( a[0]+b[0], a[1]+b[1], a[2]+b[2]); -} - -Point operator-(Point a, Point b) { - return Point( a[0]-b[0], a[1]-b[1], a[2]-b[2]); -} - -Point operator*(double a, Point b) { - return Point( a*b[0], a*b[1], a*b[2]); -} - - -bool operator <=(Point a, Point b) { - return - a[0] <= b[0] && - a[1] <= b[1] && - a[2] <= b[2]; -} - -/** - * @brief Bounding box in 3d ambient space. - * - * Primary intention is usage in BIHTree and various speedups of non-compatible - * intersections. - * - * Copy constructor and assignment are default provided by compiler. - * These can be used to set bounds latter on without particular method - * to this end: - * - * @code - * BoundingBox box; // non-initialized box - * box=BoundingBox( arma::vec3("0 1 2"), arma::vec3("4 5 6") ); - * @endcode - * - * Don;t worry about performance, all is inlined. - */ -class BoundingBox { -public: - - /// Currently we set dimension to 3. - static const unsigned int dimension = 3; - /// stabilization parameter - static constexpr double epsilon = 64 * std::numeric_limits::epsilon(); - - - - /** - * Default constructor. - * No initialization of vertices. Be very careful using this. - * One necessary usage is vector of BoundigBox. - */ - BoundingBox() {} - - /** - * Constructor for point box. - */ - BoundingBox(const Point &min) - : min_vertex_(min), max_vertex_(min) - {}; - - /** - * Constructor. - * - * From given minimal and maximal vertex. - */ - BoundingBox(const Point &min, const Point &max) - : min_vertex_(min), max_vertex_(max) - { - ASSERT( min <= max , "Wrong coordinates in constructor."); - }; - - /** - * Constructor. - * - * Make bounding box for set of points. - */ - BoundingBox(const vector &points) { - ASSERT( 0 < points.size(), "" ); - - auto it = points.begin(); - max_vertex_ = min_vertex_ = *it; - ++it; - for(; it != points.end(); ++it) expand( *it ); - } - - - /** - * Set maximum in given axis. - */ - void set_max(unsigned int axis, double max) { - ASSERT(axis < dimension, ""); - ASSERT( min(axis) <= max, ""); - max_vertex_[axis] = max; - } - - /** - * Set minimum on given axis. - */ - void set_min(unsigned int axis, double min) { - ASSERT(axis < dimension, ""); - ASSERT(min <= max(axis), ""); - min_vertex_[axis] = min; - } - - /** - * Return minimal vertex of the bounding box. - */ - const Point &min() const { - return min_vertex_; - } - - /** - * Return maximal vertex of the bounding box. - */ - const Point &max() const { - return max_vertex_; - } - - /** - * Return minimal value on given axis. - */ - double min(unsigned int axis) const { - return min()[axis]; - } - - /** - * Return maximal value on given axis. - */ - double max(unsigned int axis) const { - return max()[axis]; - } - - - /** - * Return size of the box in given axis. - */ - double size(unsigned int axis) const { - return max()[axis] - min()[axis]; - } - - - /** - * Return center of the bounding box. - */ - Point center() const { - return 0.5 * (max_vertex_ + min_vertex_); - } - - /** - * Return center of projection of the bounding box to given @p axis. - * Axis coding is: 0 - axis x, 1 - axis y, 2 - axis z. - */ - double projection_center(unsigned int axis) const{ - ASSERT(axis < dimension, ""); - return 0.5 * (max_vertex_[axis] + min_vertex_[axis]); - } - - /** - * Returns true is the box element contains @p point - * - * @param point Testing point - * @return True if box element contains point - */ - bool contains_point(const Point &point) const - { - for (unsigned int i=0; i b2.max_vertex_[i] + epsilon) || - (b2.min_vertex_[i] > max_vertex_[i] + epsilon ) ) return false; - } - return true; - } - - /** - * Returns true if projection of the box to @p axis is an interval - * less then (with tolerance) to given @p value. - */ - bool projection_lt(unsigned int axis, double value) const - { - return max_vertex_[axis] + epsilon < value; - } - - /** - * Returns true if projection of the box to @p axis is an interval - * greater then (with tolerance) to given @p value. - */ - bool projection_gt(unsigned int axis, double value) const - { - return min_vertex_[axis] - epsilon > value; - } - - /** - * Split box into two boxes along @p axis by the - * plane going through @p splitting_point on the axis. - */ - void split(unsigned int axis, double splitting_point, - BoundingBox &left, BoundingBox &right ) const - { - ASSERT(axis < dimension, ""); - if (min_vertex_[axis] <= splitting_point && splitting_point <= max_vertex_[axis] ) { - left = *this; - right = *this; - left.max_vertex_[axis] = splitting_point; - right.min_vertex_[axis] = splitting_point; - } else { - throw; - //THROW( ExcSplitting() << EI_interval_left(min_vertex_[axis]) - // << EI_interval_right(max_vertex_[axis]) - // << EI_split_point(splitting_point) ); - } - } - - /** - * Expand bounding box to contain also given @p point. - */ - void expand(const Point &point) { - for(unsigned int j=0; j diff[0]) - ? ( diff[2] > diff[1] ? 2 : 1 ) - : ( diff[2] > diff[0] ? 2 : 0 ); - } - - -private: - /// minimal coordinates of bounding box - Point min_vertex_; - /// maximal coordinates of bounding box - Point max_vertex_; -}; - -/// Overloads output operator for box. -inline ostream &operator<<(ostream &stream, const BoundingBox &box) { - stream << "Box(" - << box.min(0) << " " - << box.min(1) << " " - << box.min(2) << "; " - << box.max(0) << " " - << box.max(1) << " " - << box.max(2) << ")"; - return stream; -} - -#endif /* BOX_ELEMENT_HH_ */ diff --git a/src/BIH_tree/common.hh b/src/BIH_tree/common.hh deleted file mode 100644 index f02814a..0000000 --- a/src/BIH_tree/common.hh +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * - * Copyright (C) 2015 Technical University of Liberec. All rights reserved. - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License version 3 as published by the - * Free Software Foundation. (http://www.gnu.org/licenses/gpl-3.0.en.html) - * - * 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. - * - * - * @file bounding_box.hh - * @brief - */ - -#ifndef COMMON_HH_ -#define COMMON_HH_ - -#if DEBUG -#define ASSERT( COND, MESSAGE) - if (! (COND)) { - cout << MESSAGE; - } -#else -#define ASSERT(COND, MESSAGE) -#endif - -#endif diff --git a/src/BIH_tree/makefile b/src/BIH_tree/makefile deleted file mode 100644 index 4b67e22..0000000 --- a/src/BIH_tree/makefile +++ /dev/null @@ -1,5 +0,0 @@ -all: - $(CC) -std=c++11 -c bih_tree.cc - -clean: - rm *.o \ No newline at end of file From 2877357cf3cdd4a1ca2f836cb3054ff60a2aad88 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 31 Aug 2017 19:29:54 +0200 Subject: [PATCH 08/36] Install script. --- README.md | 13 ++++++++++++- bih_install.sh | 17 +++++++++++++++++ requirements.txt | 1 - 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 bih_install.sh diff --git a/README.md b/README.md index a51ec23..3bbb984 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,15 @@ Library focus on fast intersection algorithms for non-degenerate intersections o of small degree (especially quadratic). Requirements ------------- \ No newline at end of file +------------ + +* g++ 4.x or newer +* cmake 3.x + +In order to install BIH package locally for development just run the 'bih_install.sh' script. + +Theory +------ +[Patrikalakis-Maekawa-Cho](http://web.mit.edu/hyperbook/Patrikalakis-Maekawa-Cho/mathe.html) + + \ No newline at end of file diff --git a/bih_install.sh b/bih_install.sh new file mode 100644 index 0000000..3f38ead --- /dev/null +++ b/bih_install.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo("Installing BIH locally.") +echo("g++ 4.x and cmake 3.x or newer are assumed.") + +git submodule update --init --recursive +cd external/bih +mkdir build +cd build +cmake .. +make + +BIH_PY_PATH=`pwd` +echo "Modifiing your .bashrc:" +echo "Add "$BIH_PY_PATH" to PYTHONPATH" +echo "PYTHONPATH=\"\$PYTHONPATH:$BIH_PY_PATH\"" + diff --git a/requirements.txt b/requirements.txt index 55b033e..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +0,0 @@ -pytest \ No newline at end of file From 72715b69b54489084616089862507d7fd119eac8 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 31 Aug 2017 19:36:52 +0200 Subject: [PATCH 09/36] Fix install script, add matlab-isect submodule --- .gitmodules | 6 ++++++ bih_install.sh | 6 +++--- external/bih | 1 + external/matlab-intersections | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) mode change 100644 => 100755 bih_install.sh create mode 160000 external/bih create mode 160000 external/matlab-intersections diff --git a/.gitmodules b/.gitmodules index e69de29..4d1b072 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "external/bih"] + path = external/bih + url = git@github.com:flow123d/bih.git +[submodule "external/matlab-intersections"] + path = external/matlab-intersections + url = git@github.com:jirikopal/Intersections.git diff --git a/bih_install.sh b/bih_install.sh old mode 100644 new mode 100755 index 3f38ead..d293b8a --- a/bih_install.sh +++ b/bih_install.sh @@ -1,7 +1,7 @@ #!/bin/bash -echo("Installing BIH locally.") -echo("g++ 4.x and cmake 3.x or newer are assumed.") +echo "Installing BIH locally." +echo "g++ 4.x and cmake 3.x or newer are assumed." git submodule update --init --recursive cd external/bih @@ -13,5 +13,5 @@ make BIH_PY_PATH=`pwd` echo "Modifiing your .bashrc:" echo "Add "$BIH_PY_PATH" to PYTHONPATH" -echo "PYTHONPATH=\"\$PYTHONPATH:$BIH_PY_PATH\"" +echo "PYTHONPATH=\"\$PYTHONPATH:$BIH_PY_PATH\"" >>"${HOME}/.bashrc" diff --git a/external/bih b/external/bih new file mode 160000 index 0000000..b549511 --- /dev/null +++ b/external/bih @@ -0,0 +1 @@ +Subproject commit b54951182c73698c0bbafd37aa30e334c2ed3900 diff --git a/external/matlab-intersections b/external/matlab-intersections new file mode 160000 index 0000000..a10a099 --- /dev/null +++ b/external/matlab-intersections @@ -0,0 +1 @@ +Subproject commit a10a099c06d3033de3e5cb52e0726449fb8ac42a From b0e6a4123db253229362799a4638f9effb7a275d Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 31 Aug 2017 19:38:02 +0200 Subject: [PATCH 10/36] Remove old matlab implementation. --- matlab/basetestvec.m | 21 --- matlab/boundary_point.m | 55 ------ matlab/bounding_boxes_intersection.m | 55 ------ matlab/build_LS_matrix.m | 14 -- matlab/build_reg_matrix.m | 82 --------- matlab/compute_control_points.m | 23 --- matlab/compute_grid_lines.m | 21 --- matlab/compute_patch_edges.m | 23 --- matlab/find_int.m | 59 ------- matlab/get_errors.m | 14 -- matlab/get_knot_vector.m | 10 -- matlab/getpoints.m | 23 --- matlab/getpoints2.m | 23 --- matlab/interpolate_intersections.m | 255 --------------------------- matlab/intersections.m | 192 -------------------- matlab/patch_patch_intersection.m | 71 -------- matlab/plotresultsvec.m | 73 -------- matlab/rangetest.m | 51 ------ matlab/solve.m | 38 ---- matlab/spline_surf_vec.m | 160 ----------------- matlab/splinebasevec.m | 80 --------- matlab/swaplines.m | 8 - matlab/transform.m | 55 ------ matlab/vec2mat.m | 11 -- 24 files changed, 1417 deletions(-) delete mode 100644 matlab/basetestvec.m delete mode 100644 matlab/boundary_point.m delete mode 100644 matlab/bounding_boxes_intersection.m delete mode 100644 matlab/build_LS_matrix.m delete mode 100644 matlab/build_reg_matrix.m delete mode 100644 matlab/compute_control_points.m delete mode 100644 matlab/compute_grid_lines.m delete mode 100644 matlab/compute_patch_edges.m delete mode 100644 matlab/find_int.m delete mode 100644 matlab/get_errors.m delete mode 100644 matlab/get_knot_vector.m delete mode 100644 matlab/getpoints.m delete mode 100644 matlab/getpoints2.m delete mode 100644 matlab/interpolate_intersections.m delete mode 100644 matlab/intersections.m delete mode 100644 matlab/patch_patch_intersection.m delete mode 100644 matlab/plotresultsvec.m delete mode 100644 matlab/rangetest.m delete mode 100644 matlab/solve.m delete mode 100644 matlab/spline_surf_vec.m delete mode 100644 matlab/splinebasevec.m delete mode 100644 matlab/swaplines.m delete mode 100644 matlab/transform.m delete mode 100644 matlab/vec2mat.m diff --git a/matlab/basetestvec.m b/matlab/basetestvec.m deleted file mode 100644 index 64ec858..0000000 --- a/matlab/basetestvec.m +++ /dev/null @@ -1,21 +0,0 @@ -function [ ] = basetestvec( knots ) -n = 1000; -n_basf = length(knots)-3; -one = ones(n_basf,1); -y = sparse(zeros(n,n_basf)); -for i=1:n - y(i,:) = splinebasevec(knots,min(knots) + max(knots)*(i-1)/(n-1),0); -end -% spy(y) -% pause -x = linspace(min(knots),max(knots),n); -x -y -%spy(y) -plot(x,y) -%figure -if norm(y*one - ones(n,1))< n_basf*eps - disp('OK') -end -end - diff --git a/matlab/boundary_point.m b/matlab/boundary_point.m deleted file mode 100644 index 353766d..0000000 --- a/matlab/boundary_point.m +++ /dev/null @@ -1,55 +0,0 @@ -function [ bool ] = boundary_point(uv,iujv,u_knots,v_knots) -bool = [-1,-1]; - -uint = [u_knots(iujv(1)+2), u_knots(iujv(1)+3)]; -vint = [v_knots(iujv(2)+2), v_knots(iujv(2)+3)]; -uv; - - -if uint(1) == 0 - if abs(uv(1)-0)= mnXs) && (mnX <= mxXs)) || ... - ((mxX >= mnXs) && (mxX <= mxXs)) ) == 1 - - if (( (mnY >= mnYs) && (mnY <= mxYs)) || ... - ((mxY >= mnYs) && (mxY <= mxYs)) ) == 1 - if (( (mnZ >= mnZs) && (mnZ <= mxZs)) || ... - ((mxZ >= mnZs) && (mxZ <= mxZs)) ) == 1 - - - n_its(k,l) = n_its(k,l) + 1; - its(k,l,n_its(k,l)) = sp_i; - - end - end - end - end - end - end -end - - -end - diff --git a/matlab/build_LS_matrix.m b/matlab/build_LS_matrix.m deleted file mode 100644 index fcd2360..0000000 --- a/matlab/build_LS_matrix.m +++ /dev/null @@ -1,14 +0,0 @@ -function [ B,Interv ] = build_LS_matrix( u_knots,v_knots, Xp ) -u_n_basf = length(u_knots)-3; -v_n_basf = length(v_knots)-3; -[np k] = size(Xp); % k unused -B =spalloc(np,u_n_basf*v_n_basf,9*np); -Interv = zeros(np,2); -for j = 1:np - [uf, k] = splinebasevec(u_knots,Xp(j,1),0); - [vf, i] = splinebasevec(v_knots,Xp(j,2),0); - Interv(j,:) = [k,i]; - B(j,:) = kron(vf',uf'); -end -end - diff --git a/matlab/build_reg_matrix.m b/matlab/build_reg_matrix.m deleted file mode 100644 index 6f79078..0000000 --- a/matlab/build_reg_matrix.m +++ /dev/null @@ -1,82 +0,0 @@ -function [ A ] = build_reg_matrix( u_knots,v_knots, P0,P1,P2,P3,nnzA ) - -u_n_basf = length(u_knots)-3; -v_n_basf = length(v_knots)-3; -u_n_inter = length(u_knots) - 5 -v_n_inter = length(v_knots) - 5 - - a = P3 - P2; - b = P0 - P1; - c = P1 - P2; - d = P0 - P3; - -A =spalloc(u_n_basf*v_n_basf,u_n_basf*v_n_basf,nnzA); - -% Qpoints = [0 (1/2 - 1/sqrt(20)) (1/2 + 1/sqrt(20)) 1]; -% weights = [1/6 5/6 5/6 1/6]; - -Qpoints = [-0.90618 -0.538469 0 0.538469 0.90618] /2 + 0.5; -weights = [0.236927 0.478629 0.568889 0.478629 0.236927]; - - - -n_points = length(Qpoints); - -u_point_val = spalloc(u_n_basf,u_n_inter*n_points,u_n_inter*n_points*3); -ud_point_val = spalloc(u_n_basf,u_n_inter*n_points,u_n_inter*n_points*3); -q_u_point = zeros(u_n_inter*n_points,1); - -n = 0; -for i = 1:u_n_inter - us = u_knots(i+2); - uil = u_knots(i+3)- u_knots(i+2); - for k = 1:n_points - up = us + uil*Qpoints(k); - n = n+1; - q_u_point(n) = up; - u_point_val(:,n) = splinebasevec(u_knots,up,0,i); - ud_point_val(:,n) = splinebasevec(u_knots,up,1,i); - end -end - -%%% - -v_point_val = spalloc(v_n_basf,v_n_inter*n_points,v_n_inter*n_points*3); -vd_point_val = spalloc(v_n_basf,v_n_inter*n_points,v_n_inter*n_points*3); -q_v_point = zeros(v_n_inter*n_points,1); - -n = 0; -for i = 1:v_n_inter - vs = v_knots(i+2); - vil = v_knots(i+3)- v_knots(i+2); - for k = 1:n_points - vp = vs + vil*Qpoints(k); - n = n+1; - q_v_point(n) = vp; - v_point_val(:,n) = splinebasevec(v_knots,vp,0,i); - vd_point_val(:,n) = splinebasevec(v_knots,vp,1,i); - end -end - -%%% - -for i= 1:v_n_inter - for k = 1:n_points - v_point = v_point_val(:,(i-1)*n_points+k); - vd_point =vd_point_val(:,(i-1)*n_points+k); - for l =1:u_n_inter - for m = 1:n_points - u_point = u_point_val(:,(l-1)*n_points+m); - ud_point = ud_point_val(:,(l-1)*n_points+m); - vd = kron(vd_point,u_point); - ud = kron(v_point,ud_point); - v = q_v_point((i-1)*n_points +k); - u = q_u_point((l-1)*n_points +m); - J = det([v * a + (1 - v) * b, u * c + (1 - u) * d]); - A = A + J * weights(m)*weights(k)*(kron(ud,ud') +kron(vd,vd')); - end - end - end -end - -end diff --git a/matlab/compute_control_points.m b/matlab/compute_control_points.m deleted file mode 100644 index e93861b..0000000 --- a/matlab/compute_control_points.m +++ /dev/null @@ -1,23 +0,0 @@ -function [ X_coor,Y_coor ] = compute_control_points( u_knots, v_knots, P0,P1,P2,P3) -%UNTITLED Summary of this function goes here -% Detailed explanation goes here -u_n_basf = length(u_knots)-3; -v_n_basf = length(v_knots)-3; - - -X_coor = zeros(u_n_basf,v_n_basf); -Y_coor = zeros(u_n_basf,v_n_basf); - -for i =1:u_n_basf - u = (u_knots(i+1)+u_knots(i+2))/2; - for j =1:v_n_basf - v = (v_knots(j+1)+v_knots(j+2))/2; - P = u * (v * P2 + (1-v) * P1) + (1-u) * ( v * P3 + (1-v) * P0) ; - X_coor(i,j) = P(1); - Y_coor(i,j) = P(2); - end -end - - -end - diff --git a/matlab/compute_grid_lines.m b/matlab/compute_grid_lines.m deleted file mode 100644 index 41deea4..0000000 --- a/matlab/compute_grid_lines.m +++ /dev/null @@ -1,21 +0,0 @@ -function [ GX,GY ] = compute_grid_lines(u_knots,v_knots, P0,P1,P2,P3 ) - -u_n_grid = length(u_knots)-2; -v_n_grid = length(v_knots)-2; - -GX = zeros(u_n_grid,v_n_grid); -GY = zeros(u_n_grid,v_n_grid); - - -for i =1:u_n_grid - u = u_knots(i+2); - for j =1:v_n_grid - v = v_knots(j+2); - P = u * (v * P2 + (1-v) * P1) + (1-u) * ( v * P3 + (1-v) * P0) ; - GX(i,j) = P(1); - GY(i,j) = P(2); - end -end - -end - diff --git a/matlab/compute_patch_edges.m b/matlab/compute_patch_edges.m deleted file mode 100644 index ff068f3..0000000 --- a/matlab/compute_patch_edges.m +++ /dev/null @@ -1,23 +0,0 @@ -function [ X_coor,Y_coor ] = compute_patch_edges( u_knots, v_knots, P0,P1,P2,P3) -%UNTITLED Summary of this function goes here -% Detailed explanation goes here -u_n_bound = length(u_knots)-4; -v_n_bound = length(v_knots)-4; - - -X_coor = zeros(u_n_bound,v_n_bound); -Y_coor = zeros(u_n_bound,v_n_bound); - -for i =1:u_n_bound - u = u_knots(i+2); - for j =1:v_n_bound - v = v_knots(j+2); - P = u * (v * P2 + (1-v) * P1) + (1-u) * ( v * P3 + (1-v) * P0) ; - X_coor(i,j) = P(1); - Y_coor(i,j) = P(2); - end -end - - -end - diff --git a/matlab/find_int.m b/matlab/find_int.m deleted file mode 100644 index bfdd725..0000000 --- a/matlab/find_int.m +++ /dev/null @@ -1,59 +0,0 @@ -function [ k ] = find_int( T,t ) - -n = length(T); -mn = 3; -mx = n-2; -est = 3+floor(t*(n -5)); - -if t == T(mn) - k = mn-2; -elseif t == T(mx) - k = mx-3; -end - -if t>= T(est) - mn =est; -elseif t < T(est) % = - mx = est; -end - -s = max(ceil(log2(mx-mn)),1); - -for p=1:s+1 - if t < T(mn+1) % = - k = mn-2; - break - - elseif t>T(mx-1) % = - k = mx-3; - break - end - mid = mn + floor((mx-mn)/2); - %mn - %mx - if mid ~= mn - if t< T(mid) - mx = mid; - elseif t>= T(mid) - mn = mid; - end - else - k = mn-2; - break - end -end - -% T -% [T(mn) T(mx)] -% if (t~=0) && (t~=1) -% if (t>=T(k) && t<=T(k+1) ) -% [T T >t] -% t -% k -% disp('problem') -% pause -% end -% end - -end - diff --git a/matlab/get_errors.m b/matlab/get_errors.m deleted file mode 100644 index 266a59a..0000000 --- a/matlab/get_errors.m +++ /dev/null @@ -1,14 +0,0 @@ -function [ Err ] = get_errors(err,Interv,u_n,v_n ) - -n = length(err); -Err = zeros(v_n-2,u_n-2); -for j =1:n - Err(Interv(j,2),Interv(j,1)) = Err(Interv(j,2),Interv(j,1)) + err(j); -end - -%UNTITLED2 Summary of this function goes here -% Detailed explanation goes here - - -end - diff --git a/matlab/get_knot_vector.m b/matlab/get_knot_vector.m deleted file mode 100644 index a7bc984..0000000 --- a/matlab/get_knot_vector.m +++ /dev/null @@ -1,10 +0,0 @@ -function [ knots ] = get_knot_vector( n_basf ) - - -knots = zeros(n_basf+3,1); -knots(3:n_basf+1) = linspace(0,1,n_basf-1); -knots(n_basf+2:n_basf+3) = 1; - - -end - diff --git a/matlab/getpoints.m b/matlab/getpoints.m deleted file mode 100644 index 80211c3..0000000 --- a/matlab/getpoints.m +++ /dev/null @@ -1,23 +0,0 @@ -function [ X ] = getpoints() - -nu = 36; -nv = 26; -h = 0; -X = zeros(nu*nv,4); - -for i=1:nu - for j = 1:nv - h = h+1; - x = 2*(i-1)/(nu-1); - y = 2*(j-1)/(nv-1); - X(h,1) = x; - X(h,2) = y; - %X(h,3) = 0.3*cos(3*pi*exp(-x))*cos(3*pi*y^1.2)*cos(3*pi*sqrt(y^2+x^2)); - %X(h,3) = x^2+y^2 + 1; - X(h,3) = (2-x^2)+(2-y^2) + 1; - X(h,4) = 1; - end -end - -end - diff --git a/matlab/getpoints2.m b/matlab/getpoints2.m deleted file mode 100644 index 3f23f9a..0000000 --- a/matlab/getpoints2.m +++ /dev/null @@ -1,23 +0,0 @@ -function [ X ] = getpoints2() - -nu = 20; -nv = 20; -h = 0; -X = zeros(nu*nv,4); - -for i=1:nu - for j = 1:nv - h = h+1; - x = 2*(i-1)/(nu-1); - y = 2*(j-1)/(nv-1); - X(h,1) = x; - X(h,2) = y; - %X(h,3) = cos(10*pi*x)*cos(3*pi*y)+exp(2*sin(x*y)); - %X(h,3) = cos(3*pi*exp(-x))*cos(3*pi*y^2)+exp(2*sin(x*y)); - X(h,3) = 3*x*y; %- 0.5*rand(1,1) +10;%+exp(2*sin(x*y)); + 0.01*exp(x*y)+ - X(h,4) = 1; - end -end - -end - diff --git a/matlab/interpolate_intersections.m b/matlab/interpolate_intersections.m deleted file mode 100644 index 6089bbb..0000000 --- a/matlab/interpolate_intersections.m +++ /dev/null @@ -1,255 +0,0 @@ -%close all - -spline_surf_vec -intersections - - plot3(GXs,GYs,5*ones(us_n_basf+1,vs_n_basf+1)); - hold on - plot3(GYs,GXs,5*ones(us_n_basf+1,vs_n_basf+1)); - - for i=1:m - plot3(point(i,5),point(i,6),point(i,7),'.','MarkerSize',50); - end - -%pointx = point; -pointb = point; - - -%%% sort intersections by patches (1st patch) - - sp_i = (pointb(:,10)-1)*(us_n_intervs-1) + pointb(:,11); - -[ssp_i idx] = sort(sp_i); - -m = length(sp_i); %m-out - - - -pointb = pointb(idx,:); - -%%% Compute intersectioned patches -patch_int(1,2) = 1; -patch_int(1,1) = ssp_i(1); -%a = sp_i(1); -different = 1; -for i =2:m - if ssp_i(i) == patch_int(different,1)%a - patch_int(different,2) = patch_int(different,2) +1; - continue - else - %a = sp_i(i); - different = different +1; - patch_int(different,1) = ssp_i(i); - patch_int(different,2) = 1; - end -end - - -different; - - - -%return - - - -coinf = zeros(m,2); - - -a = 1; %?? - -%%% detect intersection point types -% -1 - interion -% 0 - global boundary -% 1 - patch boundary - -for j=1:m - coi = boundary_point(pointb(j,3:4),pointb(j,10:11),us_knots,vs_knots); - coinf(j,:) = coi; % type of interrsection (u,v) % 1D -end - -out = zeros(different,1); -point_type = zeros(m,1); % 2D - -offset = 0; -for j=1:different - for i = 1:patch_int(j,2) - - if coinf(i+offset,1) > -1 || coinf(i+offset,2) > -1 % number of boundary points for patch - out(j) = out(j) +1; - end - - % define intersection type - if coinf(i+offset,1) == -1 && coinf(i+offset,2) == -1 % internal - point_type(i+offset) = -1; - elseif coinf(i+offset,1) == 0 || coinf(i+offset,2) == 0 % global boundary - point_type(i+offset) = 0; - elseif coinf(i+offset,1) == 1 || coinf(i+offset,2) == 1 % patch internal boundary - point_type(i+offset) = 1; - end - - - - end - offset = offset + patch_int(j,2); -end - - -% number of outputs - - - -% % Sort points in patch -% offset = 0; -% for j=1:different -% -% -% patch_points= offset:offset+patch_int(j,2); -% -% boundg = find(point_type(patch_points) == 0) -% boundi = find(point_type(patch_points) == 1) -% bound = [boundg , boundi]; -% -% l = lenght(bound) -% if l >2 -% disp('more then 2 boundary points') -% end -% -% dist = zeros(patch_int(j,2)); -% -% for k = 1: offset+patch_int(j,2) -% for i = 1: offset+patch_int(j,2) -% -% dist(i) = norm(pointsb(patch_points(k),1:2)- pointsb(patch_points(i),1:2)) -% i+offset -% -% end -% -% end -% offset = offset + patch_int(j,2); -% end -% - -% Sort points in surface (create components) - - offset = 0; - bound = find(point_type == 0); - pointb = [pointb coinf point_type]; - [a,b] = size(pointb); - splinepoint = pointb; - splinepoint = swaplines(splinepoint,1,bound(1)); - - for j=1:m-1 - dist = 1/eps * ones(m,1); - for k=j+1:m - dist(k) = norm(splinepoint(j,1:2) - splinepoint(k,1:2)); - end - [a b] = min(dist); - splinepoint = swaplines(splinepoint,b,j+1); - end - - % Sort intersection on patches - boundg = find(point_type == 0); - boundl = find(point_type == 1); - bound = [boundg;boundl] - -% offset =0; -% for k=1:different -% boundg = find(point_type(1+offset:patch_int(different,2)+offset)== 0); -% boundl = find(point_type(1+offset:patch_int(different,2)+offset)== 1); -% bound = [boundg;boundl] -% %pause -% pointb = swaplines(pointb,bound(1)+offset,1+offset) -% for l=1:patch_int(k,2)-1 -% dist = 1/eps * ones(patch_int(k,2)-l,1); -% for j=1:patch_int(k,2)-l -% dist(j) = norm(splinepoint(l,1:2) - splinepoint(l+j,1:2)); -% end -% [a b] = min(dist); -% splinepoint = swaplines(splinepoint,b+offset,l+1+offset); -% -% end -% offset = offset + patch_int(k,2) -% end - - - - - %%%% Remove duplicite points () - - matrixpoints = ones(m,1); - a = 0; - for i=1:m -% if splinepoint(i,14) == 1 -% a = 1; -% end - if splinepoint(i,14)*a == 1 - matrixpoints(i) = 0; - a = 0; - elseif splinepoint(i,14) == 1 - a =1; - else - a = 0; - end - end - - -% Interpolate points -% -% -% pointb = pointb(1:m-out,:) -% -% mm = m - out; -% -% %return -% -mr = sum(matrixpoints); -XYZ = zeros(mr,3); -deltas = zeros(mr,1); -j = 0; - -for i=1:m - if matrixpoints(i)==1 - j = j+1; - XYZ(j,:) =splinepoint(i,5:7); - if j~=1 - deltas(j) = norm(XYZ(j,:)-XYZ(j-1,:)); - end - end -end - -Deltas = zeros(mr,1); - -for i=2:mr - Deltas(i)=sum(deltas(1:i)); -end - -pointspersegment=4; -nbasf = ceil(mr/pointspersegment); -t_knots = get_knot_vector(nbasf+2); -t = Deltas/Deltas(mr); -X = zeros(mr,nbasf+2); -for i=1:mr - - [tf, ~] = splinebasevec(t_knots,t(i),0); - X(i,:) = tf; -end - -sol = X\XYZ; -X*sol - XYZ; - -%X'*X\X'*XYZ - -ns = 100; -td = linspace(0,1,ns); -for i=1:ns - PT(i,:)=splinebasevec(t_knots,td(i),0)'*sol; - -end -plot3(PT(:,1),PT(:,2),PT(:,3),'r','LineWidth',3); -% -% -% -% -% -% \ No newline at end of file diff --git a/matlab/intersections.m b/matlab/intersections.m deleted file mode 100644 index 82bf21b..0000000 --- a/matlab/intersections.m +++ /dev/null @@ -1,192 +0,0 @@ - -% spline_surf_vec - %close all -% % % -% plotresultsvec(u_knots, v_knots,P0,P1,P2,P3,X,z,Err) -% hold on -% plotresultsvec(us_knots, vs_knots,P0s,P1s,P2s,P3s,Xs,zs,Errs) -hold on - - -%%%%%%%%%%%%%%%%%%%%%%%%% -%%% Compute intersections -%%%%%%%%%%%%%%%%%%%%%%%%% - -% u_n_basf = length(u_knots)-3; -% v_n_basf = length(v_knots)-3; -% us_n_basf = length(us_knots)-3; -% vs_n_basf = length(vs_knots)-3; - -u_n_intervs = u_n_basf - 2; -v_n_intervs = v_n_basf - 2; -us_n_intervs = us_n_basf - 2; -vs_n_intervs = vs_n_basf - 2; - -u_n_grid = u_n_basf+1; -v_n_grid = v_n_basf+1; -us_n_grid = us_n_basf+1; -vs_n_grid = vs_n_basf+1; - -% u_n_intervs = u_n_basf - 2; -% v_n_intervs = v_n_basf - 2; -% us_n_intervs = us_n_basf - 2; -% vs_n_intervs = vs_n_basf - 2; - -% Compute X & Y grid intersections - -%%% Grid Boundary -[ GX,GY ] = compute_grid_lines(u_knots,v_knots, P0,P1,P2,P3 ); -[ GXs,GYs ] = compute_grid_lines(us_knots,vs_knots, P0s,P1s,P2s,P3s ); - -%%% Grid centers -[ X_coor,Y_coor] = compute_control_points( u_knots, v_knots, P0,P1,P2,P3); -[ Xs_coor,Ys_coor] = compute_control_points( us_knots, vs_knots, P0s,P1s,P2s,P3s); - -%%% Compute Bounding Boxes -Z_coor = vec2mat( z,u_n_basf,v_n_basf ); -Zs_coor = vec2mat( zs,us_n_basf,vs_n_basf ); - -[patch_bound_X,patch_bound_Y] = compute_patch_edges( u_knots, v_knots, P0,P1,P2,P3); -[patch_bound_Xs,patch_bound_Ys] = compute_patch_edges( us_knots, vs_knots, P0s,P1s,P2s,P3s); - -% [ BB_X,BB_Y,BB_Z ] = compute_bounding_box( X_coor,Y_coor,Z_coor, u_n_intervs,v_n_intervs); -% [ BB_Xs,BB_Ys,BB_Zs ] = compute_bounding_box( Xs_coor,Ys_coor,Zs_coor, us_n_intervs,vs_n_intervs); - -%%% Bonding boxes intersections -[isec,n_isec] = bounding_boxes_intersection( patch_bound_X,patch_bound_Y,Z_coor,patch_bound_Xs,patch_bound_Ys,Zs_coor); - - -x = X_coor(:); -y = Y_coor(:); -xs = Xs_coor(:); -ys = Ys_coor(:); - -Xs = [xs ys zs]; - -X = [x y z]; -nt = 6; % number of main lines -n_points = 0; -ninter = zeros(u_n_intervs,v_n_intervs); -for k=1:u_n_intervs - us1 = u_knots(k+2); - ue1 = u_knots(k+3); - u1_c =(us1 + ue1)/2; - ui = [us1 ue1] ; - for l=1:v_n_intervs - vs1 = v_knots(l+2); - ve1 = v_knots(l+3); - v1_c = (vs1 + ve1)/2; - vi = [ vs1 ve1]; - s=0; - if n_isec(k,l) ~= 0 - for p =1: n_isec(k,l) - - m = ceil(isec(k,l,p) / us_n_intervs); - o = isec(k,l,p) - (m-1)*us_n_intervs; - sp_i = (m-1)*(us_n_intervs) + o; - % v2 fixed - u2i = [us_knots(m+2) us_knots(m+3)]; - v_knot = linspace(vs_knots(o+2),vs_knots(o+3),nt); - for h =1:length(v_knot) - u2_c = (us_knots(m+2) + us_knots(m+3))/2; - v2i = v_knot(h); - nit = 10; - uvu2v2 = [u1_c;v1_c;u2_c]; - [ uvu2v2,ptl,conv ] = patch_patch_intersection( uvu2v2, ui,vi,u2i,v2i, u_knots, v_knots, X,us_knots, vs_knots, Xs, nit ,m,o,k,l); - if conv ~= 0 - s = s+1; - plot3(ptl(1),ptl(2),ptl(3),'.','MarkerSize',50) - n_points = n_points +1; - point(n_points,:) = [uvu2v2',v_knot(h),ptl(1),ptl(2),ptl(3),k,l,m,o]; - end - end - - % u2 fixed - v2i = [vs_knots(o+2) vs_knots(o+3)]; - u_knot = linspace(us_knots(m+2),us_knots(m+3),nt); - for h =1:length(u_knot) - v2_c = (vs_knots(o+2) + vs_knots(o+3))/2; - u2i = u_knot(h); - nit = 10; - uvu2v2 = [u1_c;v1_c;v2_c]; - [ uvu2v2,ptl,conv ] = patch_patch_intersection( uvu2v2, ui,vi,u2i,v2i, u_knots, v_knots, X,us_knots, vs_knots, Xs, nit ,m,o,k,l); - if conv ~= 0 - s = s+1; - plot3(ptl(1),ptl(2),ptl(3),'.','MarkerSize',50) - n_points = n_points +1; - point(n_points,:) = [uvu2v2(1:2)',u_knot(h),uvu2v2(3),ptl(1),ptl(2),ptl(3),k,l,m,o]; - end - end - ninter(k,l) = s; - end - end - end -end - - ninter -%[isec,n_isec] = bounding_boxes_intersection2( patch_bound_X,patch_bound_Y,Z_coor,patch_bound_Xs,patch_bound_Ys,Zs_coor); -%[isec2,n_isec2] = bounding_boxes_intersection2( patch_bound_Xs,patch_bound_Ys,Zs_coor,patch_bound_X,patch_bound_Y,Z_coor); - -% for m=1:us_n_intervs -% us2 = us_knots(m+2); -% ue2 = us_knots(m+3); -% u2_c = (us2+ue2)/2; -% ui2 = [us2 ue2] ; -% for o=1:vs_n_intervs -% vs2 = vs_knots(o+2); -% ve2 = vs_knots(o+3); -% v2_c = (vs2+ve2)/2; -% vi2 = [ vs2 ve2]; -% s = 0; -% if n_isec2(m,o) ~= 0 -% for p =1: n_isec2(m,o) -% k = ceil(isec2(m,o,p) / u_n_intervs); -% l = isec2(m,o,p) - (k-1)*u_n_intervs; -% sp_i = (k-1)*(u_n_intervs) + l; -% % [sp_i isec2(m,o,p)] -% % pause -% t0 = 0.5; -% -% % v fixed -% v_knot = linspace(v_knots(l+2),v_knots(l+3),nt); -% for h =1:length(v_knot) -% u_val = linspace(u_knots(k+2),u_knots(k+3),3); -% v_val = v_knot(h); -% XYZ = get_span(u_knots,v_knots,X,u_val,v_val); -% nit = 6; -% uvt0 = [u2_c;v2_c;t0]; -% [ uvtl, ptl ,conv] = compute_initial_guess( uvt0, ui2,vi2,us_knots, vs_knots, Xs, XYZ, nit,m,o); -% if conv ~= 0 -% s = s+1; -% plot3(ptl(1),ptl(2),ptl(3),'.','MarkerSize',50) -% end -% end -% -% % u fixed -% u_knot = linspace(u_knots(k+2),u_knots(k+3),nt); -% for h =1:length(u_knot) -% u_val = u_knot(h); -% v_val = linspace(v_knots(l+2),v_knots(l+3),3); -% XYZ = get_span(u_knots,v_knots,X,u_val,v_val); -% nit = 6; -% uvt0 = [u2_c;v2_c;t0]; -% [ uvtl, ptl ,conv] = compute_initial_guess( uvt0, ui2,vi2,us_knots, vs_knots, Xs, XYZ, nit,m,o); -% if conv ~= 0 -% s = s+1; -% plot3(ptl(1),ptl(2),ptl(3),'.','MarkerSize',50) -% end -% end -% -% ninter(m,o) = ninter(m,o) + s; -% end -% end -% end -% end - -%t = toc - -%cas = sum(sum(ninter))/t - -ninter - plot3(GXs,GYs,5*ones(us_n_basf+1,vs_n_basf+1)) - plot3(GYs,GXs,5*ones(us_n_basf+1,vs_n_basf+1)) diff --git a/matlab/patch_patch_intersection.m b/matlab/patch_patch_intersection.m deleted file mode 100644 index b39ea02..0000000 --- a/matlab/patch_patch_intersection.m +++ /dev/null @@ -1,71 +0,0 @@ -function [ uvt,pt,conv ] = patch_patch_intersection( uvt, ui,vi,u2i,v2i,u_knots, v_knots, X,us_knots, vs_knots, Xs, nit ,m,o,k,l) - -pt =zeros(3,1); -conv =0; -tol = 1e-4; -tol2 = 1e-6; - -if length(u2i) == 1 - [u2f, ~] = splinebasevec(us_knots,u2i,0,m); -end -if length(v2i) == 1 - [v2f, ~] = splinebasevec(vs_knots,v2i,0,o); -end - - -for i=1:nit - [uf, ~] = splinebasevec(u_knots,uvt(1),0,k); - [vf, ~] = splinebasevec(v_knots,uvt(2),0,l); - [ufd, ~] = splinebasevec(u_knots,uvt(1),1,k); - [vfd, ~] = splinebasevec(v_knots,uvt(2),1,l); - - if length(u2i) == 1 - [v2f, ~] = splinebasevec(vs_knots,uvt(3),0,o); - [v2fd, ~] = splinebasevec(vs_knots,uvt(3),1,o); - dXYZp2 = (kron(v2fd',u2f')*Xs)'; - end - if length(v2i) == 1 - [u2f, ~] = splinebasevec(us_knots,uvt(3),0,m); - [u2fd, ~] = splinebasevec(us_knots,uvt(3),1,m); - dXYZp2 = (kron(v2f',u2fd')*Xs)'; - end - - dXYZu1 = (kron(vf',ufd')*X)'; % - dXYZv1 = (kron(vfd',uf')*X)'; % - - J = [dXYZu1 dXYZv1 -dXYZp2]; - - deltaXYZ = (kron(vf',uf') * X)' - (kron(v2f',u2f') * Xs)'; - uvt = uvt- J\deltaXYZ; - %if i~=nit - [test,uvt] = rangetest(uvt,ui,vi,u2i,v2i,0.0); - %end -end - -[test,uvt] = rangetest(uvt,ui,vi,u2i,v2i,tol2); -if test == 1 - [uf, ~] = splinebasevec(u_knots,uvt(1),0,k); - [vf, ~] = splinebasevec(v_knots,uvt(2),0,l); - - if length(u2i) == 1 - [v2f, ~] = splinebasevec(vs_knots,uvt(3),0,o); - end - if length(v2i) == 1 - [u2f, ~] = splinebasevec(us_knots,uvt(3),0,m); - end - - deltaXYZ = (kron(vf',uf') * X)' - (kron(v2f',u2f') * Xs)'; - dist = norm(deltaXYZ); -end - -if test == 1 - if dist <= tol - pt =kron(vf',uf')*X; - conv =1; - end -else - uvt = zeros(3,1); -end - -end - diff --git a/matlab/plotresultsvec.m b/matlab/plotresultsvec.m deleted file mode 100644 index 7d510b8..0000000 --- a/matlab/plotresultsvec.m +++ /dev/null @@ -1,73 +0,0 @@ -function [ ] = plotresultsvec( u_knots, v_knots,P0,P1,P2,P3,X,z, Err) - -[np k] = size(X); - -u_n_basf = length(u_knots)-3; -v_n_basf = length(v_knots)-3; - - -nu = 60; -nv = 60; - -Zsurf = zeros(nu,nv); -Xsurf = zeros(nu,nv); -Ysurf = zeros(nu,nv); - -% Compute X & Y control points - - -[ X_coor,Y_coor] = compute_control_points( u_knots, v_knots, P0,P1,P2,P3); - -x = X_coor(:); -y = Y_coor(:); - - -% Compute fine grid in X & Y & Z for draw - -uf = spalloc(u_n_basf,nu,3*nu); -vf = spalloc(v_n_basf,nv,3*nv); - -for j =1:nu - u = (j-1)/(nu-1); - uf(:,j) = splinebasevec(u_knots,u,0); -end - -for k =1:nv - v = (k-1)/(nv-1); - vf(:,k) = splinebasevec(v_knots,v,0); -end - -for k =1:nv - Zsurf(:,k) = kron(vf',uf(:,k)') * z; - Xsurf(:,k) = kron(vf',uf(:,k)') * x; - Ysurf(:,k) = kron(vf',uf(:,k)') * y; -end - - - -surf(Xsurf,Ysurf,Zsurf); - - -%%plot original points - -% hold on -% for k=1:np -% if X(k,4) ~=0 -% plot3(X(k,1),X(k,2),X(k,3),'.k','MarkerSize',25); -% end -% end - -%% - -% hold off -% -% figure -% -% Xsurferr = kron(xv(2:u_n_basf-1)',ones(v_n_basf-2,1)); -% Ysurferr = kron(yv(2:v_n_basf-1),ones(u_n_basf-2,1)'); -% -% -% surf(Xsurferr,Ysurferr,Err); - -end - diff --git a/matlab/rangetest.m b/matlab/rangetest.m deleted file mode 100644 index af8ef7c..0000000 --- a/matlab/rangetest.m +++ /dev/null @@ -1,51 +0,0 @@ -function [ test, uvt ] = rangetest( uvt,ui,vi,u2i,v2i,tol ) - - -test = 0; - -du = [uvt(1)-ui(1), ui(2)- uvt(1)]; -dv = [uvt(2)-vi(1), vi(2)- uvt(2)]; - -if length(v2i) == 1 -d2p = [uvt(3)-u2i(1), u2i(2)- uvt(3)]; -pi = u2i; -end - -if length(u2i) == 1 -d2p = [uvt(3)-v2i(1), v2i(2)- uvt(3)]; -pi = v2i; -end - -%dv = [uvt(2)-vi(1), vi(2)- uvt(2)]; - - -for i =1:2 - if (du(i) < -tol) - uvt(1) = ui(i); - end -end - -for i =1:2 - if (dv(i) < -tol) - uvt(2) = vi(i); - end -end - -for i =1:2 - if (d2p(i) < -tol) - uvt(3) = pi(i); - end -end - - -if (uvt(1)>=ui(1)) && (uvt(1)<=ui(2)) - if (uvt(2)>=vi(1)) && (uvt(2)<=vi(2)) - if ((uvt(3)>=pi(1)) && (uvt(3)<=pi(2))) - test = 1; - end - end -end - - -end - diff --git a/matlab/solve.m b/matlab/solve.m deleted file mode 100644 index 7f1d395..0000000 --- a/matlab/solve.m +++ /dev/null @@ -1,38 +0,0 @@ -function [ Xp ] = solve( Xp,P0,P1,P2,P3 ) - -[np k] = size(Xp); -Pi = [P0 P1 P2 P3]; - -%%% Compute local coordinates and drop all - -A = P3 - P2; -B = P0 - P1; -C = P1 - P2; -D = P0 - P3; - -for j=1:np - - P = [Xp(j,5); Xp(j,6)]; - - u = Xp(j,1); - v = Xp(j,2); - - uv = [u;v]; - - iters = 10; - - for i = 1:iters - u = uv(1); - v = uv(2); - - % J = [ v * A(1) + (1-v) * B(1), u * C(1) + (1 - u) * D(1); - % v * A(2) + (1-v) * B(2), u * C(2) + (1 - u) * D(2)]; - J = [ v * A + (1-v) * B, u * C + (1 - u) * D]; - Fxy = P - u * (v * P2 + (1-v) * P1) - (1-u) * ( v * P3 + (1-v) * P0); - uv = [u;v] - inv(J) * Fxy; - end - Xp(j,1) = uv(1); - Xp(j,2) = uv(2); -end -end - diff --git a/matlab/spline_surf_vec.m b/matlab/spline_surf_vec.m deleted file mode 100644 index 5d067a8..0000000 --- a/matlab/spline_surf_vec.m +++ /dev/null @@ -1,160 +0,0 @@ - -clc -clear all -close all - - -u_n_basf = 15; -v_n_basf = 15; -u_knots = get_knot_vector(u_n_basf); -v_knots = get_knot_vector(v_n_basf); - -%%% base test - -% basetestvec(v_knots); -% return -%%% patch boundary - -% P0 = [0.2; 0.3]; -% P1 = [1.2; 0.5]; -% P2 = [1.5; 2]; -% P3 = [0.9; 1.7]; - -P0 = [0.0; 0.0]; -P1 = [2.0; 0.0]; -P2 = [2.0; 2.0]; -P3 = [0.0; 2.0]; - -%%% Reduce points to be approximatex (i.e., points which are in convex hull of the points P0,P1,P2,P3) - -X = getpoints(); -[Xp X] = transform( X,P0,P1,P2,P3 ); -[np k] = size(Xp); -[ Xp ] = solve( Xp,P0,P1,P2,P3 ); - -%%% Construction of the matrix - - -%interv ?? -[B, Interv ] = build_LS_matrix( u_knots,v_knots, Xp); - -W = sparse(diag(Xp(:,4))); - -g = Xp(:,3); -b = B'*W*g; - -C = B'*W*B; -nnzC = nnz(C); - - -A = build_reg_matrix( u_knots,v_knots, P0,P1,P2,P3,nnzC); - -nC = norm(full(C)); -nA = norm(full(A)); -r = norm(full(C))/ norm(full(A)); -%r = 0.0; - -% [q r] = qr(B); -% -% -% z = r\(q'*g) - -S = C+0.01*r*A; - -z = pcg(S,b,1e-12,500); - -%%% Solution - -% Direct Solver -% C = B'*W*B;%+A; -% b = B'*W*g; -% z = C\b; - - -% Errors -Err = get_errors(abs(W*B*z-g),Interv,u_n_basf,v_n_basf); - - -%%% PLOT results - -plotresultsvec(u_knots, v_knots,P0,P1,P2,P3,X,z,Err) - -%return - -%%%%%%%%%%%%%%%%%% -%%% Second Surface -%%%%%%%%%%%%%%%%%% - - -us_n_basf = 10; -vs_n_basf = 10; -us_knots = get_knot_vector(us_n_basf); -vs_knots = get_knot_vector(vs_n_basf); - -%%% patch boundary - -% P0s = [0.4; 0.2]; -% P1s = [1.5; 0.1]; -% P2s = [1.1; 1.8]; -% P3s = [0.6; 1.7]; - -P0s = [0.0; 0.0]; -P1s = [2.0; 0.0]; -P2s = [2.0; 2.0]; -P3s = [0.0; 2.0]; - - -%%% Reduce points to be approximatex (i.e., points which are in convex hull of the points P0,P1,P2,P3) - -Xs = getpoints2(); -[Xps Xs] = transform( Xs,P0s,P1s,P2s,P3s ); -[nps ks] = size(Xps); -[ Xps ] = solve( Xps,P0s,P1s,P2s,P3s ); - -%%% Construction of the matrix - -%interv ?? -[Bs, Intervs ] = build_LS_matrix( us_knots,vs_knots, Xps); - -Ws = sparse(diag(Xps(:,4))); - -gs = Xps(:,3); -bs = Bs'*Ws*gs; - -Cs = Bs'*Ws*Bs; -nnzCs = nnz(Cs); - - -As = build_reg_matrix( us_knots,vs_knots, P0s,P1s,P2s,P3s,nnzCs); - -nCs= norm(full(Cs)); -nAs = norm(full(As)); -rs = norm(full(Cs))/ norm(full(As)); -%rs = 0.0; - -Ss = Cs+0.0010*rs*As; - -zs = pcg(Ss,bs,1e-14,500); -%zs = Ss\bs; - -%%% Solution - -% Direct Solver -% C = B'*W*B;%+A; -% b = B'*W*g; -% z = C\b; - - -% Errors -Errs = get_errors(abs(Ws*Bs*zs-gs),Intervs,us_n_basf,vs_n_basf); - - -%%% PLOT results -hold on -plotresultsvec(us_knots, vs_knots,P0s,P1s,P2s,P3s,Xs,zs,Errs) -hold off - - -%intersections - -% \ No newline at end of file diff --git a/matlab/splinebasevec.m b/matlab/splinebasevec.m deleted file mode 100644 index 007e303..0000000 --- a/matlab/splinebasevec.m +++ /dev/null @@ -1,80 +0,0 @@ -function [ f, k ] = splinebasevec( T,t,order,varargin) - -% T - knot vector -% k - index of basis function, k = 1,...,length(T)-3 -% t - parameter t \in [0,1] -% f - vector of basis functions values -% k - kth interval - -n = length(T); - -f = spalloc(n-3,1,3); - -optargin = length(varargin); - -if optargin == 0 - k = find_int( T,t ); -elseif optargin == 1 - k = varargin{1}; -% if (T(k) <= t+2) && (T(k+3) >= t) -% disp('OK') -% [T(k+2), t,T(k+3)] -% else -% disp('PROBLEM') -% return -% end -if (T(k) <= t+2) && (T(k+3) >= t) - % disp('OK'); - [T(k+2), t,T(k+3)]; -else - disp('PROBLEM') - pause - return -end -end - - -% for k = 1:n-5 -% if(t>=T(k+2) && t<=T(k+3)) -% % k_int = k; -% break; -% end -% end -% -% if k ~=k2-2 -% [k k2-2] -% pause -% end - -tk1 = T(k+1); -tk2 = T(k+2); -tk3 = T(k+3); -tk4 = T(k+4); - -d31 = tk3-tk1; -d32 = tk3-tk2; -d42 = tk4-tk2; - -d3t = tk3-t; -dt1 = t-tk1; -dt2 = t-tk2; -d4t = tk4-t; - -d31d32 = d31*d32; -d42d32 = d42*d32; - -% f(k) = (tk3-t)^2/((tk3-tk1)*(tk3-tk2)); -% f(k+1)= (t-tk1)*(tk3 -t)/((tk3-tk1)*(tk3-tk2)) + (t-tk2)*(tk4 -t)/((tk4-tk2)*(tk3-tk2)); -% f(k+2) = (t-tk2)^2/((tk4 - tk2)*(tk3-tk2)); - -if order == 0 - f(k) = d3t^2/d31d32; - f(k+1)= dt1*d3t/d31d32 + dt2*d4t/d42d32; - f(k+2) = dt2^2/d42d32; -elseif order ==1 - f(k) = -2*d3t/d31d32; - f(k+1)= (d3t - dt1)/d31d32 + (d4t - dt2)/d42d32; - f(k+2) = 2*dt2/d42d32; -end - -end \ No newline at end of file diff --git a/matlab/swaplines.m b/matlab/swaplines.m deleted file mode 100644 index b9ad811..0000000 --- a/matlab/swaplines.m +++ /dev/null @@ -1,8 +0,0 @@ -function [ mat ] = swaplines( mat,i,j ) - -temp = mat(j,:); -mat(j,:) = mat(i,:); -mat(i,:)= temp; - -end - diff --git a/matlab/transform.m b/matlab/transform.m deleted file mode 100644 index 932957f..0000000 --- a/matlab/transform.m +++ /dev/null @@ -1,55 +0,0 @@ -function [ Xp X] = transform( X,P0,P1,P2,P3 ) - -[np k] = size(X); - -Pi = [P0 P1 P2 P3]; - -%%% Compute normalized normals - -Ni = zeros(2,4); - - -Ni(:,1) = Pi(:,1) - Pi(:,4); -nt = Ni(1,1); -Ni(1,1) = -Ni(2,1); -Ni(2,1) = nt; -Ni(:,1) = Ni(:,1)/norm(Ni(:,1)); - - - -for i =2:4 - Ni(:,i) = Pi(:,i) - Pi(:,i-1); - nt = Ni(1,i); - Ni(1,i) = -Ni(2,i); - Ni(2,i) = nt; - Ni(:,i) = Ni(:,i)/norm(Ni(:,i)); -end - -%%% Compute local coordinates and drop all - -h = 0; -for j=1:np - - P = [X(j,1); X(j,2)]; - - u = (P - Pi(:,1))' * Ni(:,1) / ( (P - Pi(:,1))' * Ni(:,1) + (P - Pi(:,3))' * Ni(:,3) ) ; - v = (P - Pi(:,1))' * Ni(:,2) / ( (P - Pi(:,1))' * Ni(:,2) + (P - Pi(:,4))' * Ni(:,4) ) ; -% P - Pi(:,1) -% P - Pi(:,4) -% Ni(:,2) -% Ni(:,4) -% (P - Pi(:,1))' * Ni(:,2) -% (P - Pi(:,4))' * Ni(:,4) -% pause - - if (u >= 0) && (u <= 1) && (v >= 0) && (v <= 1) - h = h+1; - Xp(h,1) = u; - Xp(h,2) = v; - Xp(h,3:4) = X(j,3:4); - Xp(h,5:6) = X(j,1:2); - end -end - -end - diff --git a/matlab/vec2mat.m b/matlab/vec2mat.m deleted file mode 100644 index 7f0d202..0000000 --- a/matlab/vec2mat.m +++ /dev/null @@ -1,11 +0,0 @@ -function [ Z_coor ] = vec2mat( zs,u_n_basf,v_n_basf ) -%UNTITLED3 Summary of this function goes here -% Detailed explanation goes here -Z_coor = zeros(u_n_basf,v_n_basf); - -for i =1:v_n_basf - Z_coor(:,i) = zs(1+(i-1)*(u_n_basf):i*(u_n_basf)); -end - -end - From 1ca90a9169944d6054e836d2c5fe43308a1c2a84 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 11 Sep 2017 10:09:24 +0200 Subject: [PATCH 11/36] Move forward to new version of matlab code --- external/matlab-intersections | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/matlab-intersections b/external/matlab-intersections index a10a099..e1d242e 160000 --- a/external/matlab-intersections +++ b/external/matlab-intersections @@ -1 +1 @@ -Subproject commit a10a099c06d3033de3e5cb52e0726449fb8ac42a +Subproject commit e1d242e4c2e43966b6dfda0e9c82ee3f8a4ddf03 From a9727d785132b11fb6b04614735e9c58b894c43e Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 11 Sep 2017 17:25:40 +0200 Subject: [PATCH 12/36] Update BIH submodule --- external/bih | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/bih b/external/bih index b549511..2d7b774 160000 --- a/external/bih +++ b/external/bih @@ -1 +1 @@ -Subproject commit b54951182c73698c0bbafd37aa30e334c2ed3900 +Subproject commit 2d7b774bae113c23e07b39b59253f9c49cce075e From 82d687a2f324dce103c298c6b22b0099f321355e Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 11 Sep 2017 17:35:53 +0200 Subject: [PATCH 13/36] Fix tracking of matlab-intersections --- external/matlab-intersections | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/matlab-intersections b/external/matlab-intersections index e1d242e..2bf73a4 160000 --- a/external/matlab-intersections +++ b/external/matlab-intersections @@ -1 +1 @@ -Subproject commit e1d242e4c2e43966b6dfda0e9c82ee3f8a4ddf03 +Subproject commit 2bf73a4a4f65aefdc17c955c84471897b67bc32a From 2f2e44bdfa213a706e03acc5de65c1232bbd1d11 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Fri, 15 Sep 2017 18:45:56 +0200 Subject: [PATCH 14/36] Classes for general B-spline and rational curves and surfaces. - representation and avaluation of curves and surfaces - plotting module - basic tests (mainly via plotting) --- src/bs_curve.py | 12 - src/bs_surface.py | 5 - src/bspline.py | 700 ++++++++++++++++++++++++++++++++++++++++++ src/bspline_plot.py | 84 +++++ tests/test_bspline.py | 133 ++++++++ 5 files changed, 917 insertions(+), 17 deletions(-) delete mode 100644 src/bs_curve.py delete mode 100644 src/bs_surface.py create mode 100644 src/bspline.py create mode 100644 src/bspline_plot.py create mode 100644 tests/test_bspline.py diff --git a/src/bs_curve.py b/src/bs_curve.py deleted file mode 100644 index 7a38ed0..0000000 --- a/src/bs_curve.py +++ /dev/null @@ -1,12 +0,0 @@ -class BSCurve: - ''' - Class representing a general B-spline curve. - Specializations for particular degree are derived. - ''' - pass - - -class BSCurveD2: - ''' - Quadratic B-spline. - ''' \ No newline at end of file diff --git a/src/bs_surface.py b/src/bs_surface.py deleted file mode 100644 index c10a266..0000000 --- a/src/bs_surface.py +++ /dev/null @@ -1,5 +0,0 @@ -class BSSurface: - ''' - Representation of the B-spline surface. - ''' - pass \ No newline at end of file diff --git a/src/bspline.py b/src/bspline.py new file mode 100644 index 0000000..4aa0271 --- /dev/null +++ b/src/bspline.py @@ -0,0 +1,700 @@ +""" +Module with classes representing various B-spline and NURMS curves and surfaces. +These classes provide just basic functionality: +- storing the data +- evaluation of XYZ for UV +- derivatives +In future: +- serialization and deserialization using JSONdata - must make it an installable module +""" + +""" +This module tries to approximate 2.5D array of terrain points +using B-Spline surface. +""" + +import matplotlib.pyplot as plt +import numpy as np +import math +import numpy.linalg as la + + +__author__ = 'Jan Brezina , Jiri Hnidek , Jiri Kopal ' + + +class ParamError(Exception): + pass + +def check_matrix(mat, shape, values, idx=[]): + ''' + Check shape and type of scalar, vector or matrix. + :param mat: Scalar, vector, or vector of vectors (i.e. matrix). Vector may be list or other iterable. + :param shape: List of dimensions: [] for scalar, [ n ] for vector, [n_rows, n_cols] for matrix. + If a value in this list is None, the dimension can be arbitrary. The shape list is set fo actual dimensions + of the matrix. + :param values: Type or tuple of allowed types of elements of the matrix. E.g. ( int, float ) + :param idx: Internal. Used to pass actual index in the matrix for possible error messages. + :return: + ''' + try: + if len(shape) == 0: + if not isinstance(mat, values): + raise ParamError("Element at index {} of type {}, expected instance of {}.".format(idx, type(mat), values)) + else: + if shape[0] is None: + shape[0] = len(mat) + l=None + if not hasattr(mat, '__len__'): + l=0 + elif len(mat) != shape[0]: + l=len(mat) + if not l is None: + raise ParamError("Wrong len {} of element {}, should be {}.".format(l, idx, shape[0])) + for i, item in enumerate(mat): + sub_shape = shape[1:] + check_matrix(item, sub_shape, values, idx = [i] + idx) + shape[1:] = sub_shape + return shape + except ParamError: + raise + except Exception as e: + raise ParamError(e) + + +def check_knots(deg, knots, N): + total_multiplicity = 0 + for knot, mult in knots: + # This condition must hold if we assume only (0,1) interval of curve or surface parameters. + #assert float(knot) >= 0.0 and float(knot) <= 1.0 + total_multiplicity += mult + assert total_multiplicity == deg + N + 1 + + +scalar_types = (int, float, np.int64) + + + + + + + + + + + + + + +class SplineBasis: + """ + Represents a spline basis for a given knot vector and degree. + Provides canonical evaluation for the bases functions and their derivatives, knot vector lookup etc. + """ + + @classmethod + def make_equidistant(cls, degree, n_intervals, knot_range=[0.0, 1.0]): + """ + Returns spline basis for an eqidistant knot vector + having 'n_intervals' subintervals. + :param degree: degree of the spline basis + :param n_intervals: length of vector + :param knot_range: support of the spline, min and max valid 't' + :return: np array of knots + """ + n = n_intervals + 2 * degree + 1 + knots = np.array((knot_range[0],) * n) + diff = (knot_range[1] - knot_range[0]) / n_intervals + for i in range(degree + 1, n - degree): + knots[i] = (i - degree) * diff + knot_range[0] + knots[-degree - 1:] = knot_range[1] + return cls(degree, knots) + + @classmethod + def make_from_packed_knots(cls, degree, knots): + full_knots = [ q for q, mult in knots for i in range(mult) ] + return cls(degree, full_knots) + + + def __init__(self, degree, knots): + """ + Constructor of the basis. + :param degree: Degree of Bezier polynomials >=0. + :param knots: Numpy array of the knots including multiplicities. + """ + assert degree >=0 + self.degree = degree + + # check free ends (and full degree along the whole curve) + for i in range(self.degree): + assert knots[i] == knots[i+1] + assert knots[-i-1] == knots[-i-2] + self.knots = knots + + self.size = len(self.knots) - self.degree -1 + self.knots_idx_range = [self.degree, len(self.knots) - self.degree - 1] + self.domain = self.knots[self.knots_idx_range] + self.domain_size = self.domain[1] - self.domain[0] + # Number of basis functions. + + def pack_knots(self): + last, mult = self.knots[0], 0 + packed_knots = [] + for q in self.knots: + if q == last: + mult+=1 + else: + packed_knots.append( (last, mult) ) + last, mult = q, 1 + return packed_knots + + def find_knot_interval(self, t): + """ + Find the first non-empty knot interval containing the value 't'. + i.e. knots[i] <= t <= knots[i+1], where knots[i] < knots[i+1] + Returns I = i - degree, which is the index of the first basis function + nonzero in 't'. + + :param t: float, must be within knots limits. + :return: I + """ + assert self.knots[0] <= t <= self.knots[-1] + + """ + This function try to find index for given t_param in knot_vec that + is covered by all (3) base functions. + :param self.knots: + :param t: + :return: + """ + + # get range without multiplicities + mn = self.knots_idx_range[0] + mx = self.knots_idx_range[1] + diff = mx - mn + + while diff > 1: + # estimate for equidistant knots + t_01 = (t - self.knots[mn]) / self.domain_size + est = int(t_01 * diff + mn) + if t > self.knots[est]: + if mn == est : + break + mn = est + else: + if mx == est: + mn = mx + break + mx = est + diff = mx - mn + return min(mn, self.knots_idx_range[1] - 1) - self.degree + + def _basis(self, deg, idx, t): + """ + Recursive evaluation of basis function of given degree and index. + + :param deg: Degree of the basis function + :param idx: Index of the basis function to evaluate. + :param t: Point of evaluation. + :return Value of the basis function. + """ + + if deg == 0: + t_0 = self.knots[idx] + t_1 = self.knots[idx + 1] + value = 1.0 if t_0 <= t < t_1 else 0.0 + return value + else: + t_i = self.knots[idx] + t_ik = self.knots[idx + deg] + top = t - t_i + bottom = t_ik - t_i + if bottom != 0: + value = top / bottom * self._basis(deg-1, idx, t) + else: + value = 0.0 + + t_ik1 = self.knots[idx + deg + 1] + t_i1 = self.knots[idx + 1] + top = t_ik1 - t + bottom = t_ik1 - t_i1 + if bottom != 0: + value += top / bottom * self._basis(deg-1, idx+1, t) + + return value + + def fn_supp(self, i_base): + """ + Support of the base function 'i_base'. + :param i_base: + :return: (t_min, t_max) + """ + return (self.knots[i_base], self.knots[i_base + self.degree + 1]) + + def eval(self, i_base, t): + """ + :param i_base: Index of base function to evaluate. + :param t: point in which evaluate + :return: b_i(y) + """ + assert 0 <= i_base < self.size + if i_base == self.size -1 and t == self.domain[1]: + return 1.0 + return self._basis(self.degree, i_base, t) + + + +class Curve: + + @classmethod + def make_raw(cls, poles, knots, rational=False, degree=2): + """ + Construct a B-spline curve. + :param poles: List of poles (control points) ( X, Y, Z ) or weighted points (X,Y,Z, w). X,Y,Z,w are floats. + Weighted points are used only for rational B-splines (i.e. nurbs) + :param knots: List of tuples (knot, multiplicity), where knot is float, t-parameter on the curve of the knot + and multiplicity is positive int. Total number of knots, i.e. sum of their multiplicities, must be + degree + N + 1, where N is number of poles. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. + :param degree: Non-negative int + """ + basis = SplineBasis(degree, knots) + return cls(basis, poles, rational) + + """ + Defines a 3D (or (dim -D) curve as B-spline. We shall work only with B-splines of degree 2. + Corresponds to "B-spline Curve - <3D curve record 7>" from BREP format description. + """ + def __init__(self, basis, poles, rational = False): + self.basis = basis + dim = len(poles[0]) - rational + check_matrix(poles, [self.basis.size, dim + rational], scalar_types ) + + self.poles=np.array(poles) # N x D + self.rational=rational + if rational: + self._weights = poles[:, dim] + self._poles = (poles[:, 0:dim].T * self._weights ).T + + + def eval(self, t): + + it = self.basis.find_knot_interval(t) + dt = self.basis.degree + 1 + t_base_vec = np.array([self.basis.eval(jt, t) for jt in range(it, it + dt)]) + + if self.rational: + top_value = np.inner(t_base_vec, self._poles[it: it + dt, :].T) + bot_value = np.inner(t_base_vec, self._weights[it: it + dt]) + return top_value / bot_value + else: + return np.inner(t_base_vec, self.poles[it: it + dt, :].T) + + + + + +class Surface: + """ + Defines a B-spline surface. + """ + + @classmethod + def make_raw(cls, poles, knots, rational=False, degree=(2,2)): + """ + Construct a B-spline curve. + :param poles: List of poles (control points) ( X, Y, Z ) or weighted points (X,Y,Z, w). X,Y,Z,w are floats. + Weighted points are used only for rational B-splines (i.e. nurbs) + :param knots: List of tuples (knot, multiplicity), where knot is float, t-parameter on the curve of the knot + and multiplicity is positive int. Total number of knots, i.e. sum of their multiplicities, must be + degree + N + 1, where N is number of poles. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. + :param degree: Non-negative int + """ + u_basis = SplineBasis(degree[0], knots[0]) + v_basis = SplineBasis(degree[0], knots[0]) + return cls( (u_basis, v_basis), poles, rational) + + + def __init__(self, basis, poles, rational=False): + """ + Construct a B-spline in 3d space. + :param poles: Matrix (list of lists) of Nu times Nv poles (control points). + Single pole is a points ( X, Y, Z ) or weighted point (X,Y,Z, w). X,Y,Z,w are floats. + Weighted points are used only for rational B-splines (i.e. nurbs) + :param knots: Tuple (u_knots, v_knots). Both u_knots and v_knots are lists of tuples + (knot, multiplicity), where knot is float, t-parameter on the curve of the knot + and multiplicity is positive int. For both U and V knot vector the total number of knots, + i.e. sum of their multiplicities, must be degree + N + 1, where N is number of poles. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. BREP format have two independent flags + for U and V parametr, but only choices 0,0 and 1,1 have sense. + :param degree: (u_degree, v_degree) Both positive ints. + """ + + self.u_basis, self.v_basis = basis + self.rational = rational + dim = len(poles[0][0]) - rational + check_matrix(poles, [self.u_basis.size, self.v_basis.size, dim + rational], scalar_types ) + self.poles=np.array(poles) + assert self.poles.shape == (self.u_basis.size, self.v_basis.size, dim + rational) + if rational: + self._weights = poles[:, :, dim] + self._poles = (poles[:,:,0:dim].T * self._weights.T ).T + + def eval(self, u, v): + iu = self.u_basis.find_knot_interval(u) + iv = self.v_basis.find_knot_interval(v) + du = self.u_basis.degree + 1 + dv = self.v_basis.degree + 1 + u_base_vec = np.array([self.u_basis.eval(ju, u) for ju in range(iu, iu + du)]) + v_base_vec = np.array([self.v_basis.eval(jv, v) for jv in range(iv, iv + dv)]) + + if self.rational: + top_value = np.inner(u_base_vec, self._poles[iu: iu + du, iv: iv + dv, :].T) + top_value = np.inner(top_value, v_base_vec) + bot_value = np.inner(u_base_vec, self._weights[iu: iu + du, iv: iv + dv].T) + bot_value = np.inner(bot_value, v_base_vec) + return top_value / bot_value + else: + #print("u: {} v: {} p: {}".format(u_base_vec.shape, v_base_vec.shape, self.poles[iu: iu + du, iv: iv + dv, :].shape)) + # inner product sums along the last axis of its parameters + value = np.inner(u_base_vec, self.poles[iu: iu + du, iv: iv + dv, :].T ) + #print("val: {}".format(value.shape)) + return np.inner( value, v_base_vec) + + + + +class Z_Surface: + """ + Simplified B-spline surface that use just linear or bilinear transform between XY and UV. + + TODO: + - We need conversion to full 3D surface for the BREP output + - Optimization: simplified Bspline evaluation just for the singel coordinate + """ + + def eval_xy(self, xy_points): + pass + + + + + + + + + + + + + + + + + + + + + + + + + + +# Cache used of knot vector (computation of differences) +KVN_CACHE = {} +SB_CACHE = {} + + + +def spline_base_vec(knot_vec, t_param, order, sparse=False): + """ + This function compute normalized blending function aka base function of B-Spline curve or surface. + :param knot_vec: + :param t_param: + :param order: (0: function value, 1: derivative function value) + :param sparse: + :return: + """ + + + idx = find_index(knot_vec, t_param) + n_basf = len(knot_vec) - 3 + + # Create sparse matrix + if sparse is True: + basis_values = np.zeros(3) + else: + basis_values = np.zeros(n_basf) + + tk1 = knot_vec[idx + 1] + tk2 = knot_vec[idx + 2] + tk3 = knot_vec[idx + 3] + tk4 = knot_vec[idx + 4] + + d31 = tk3 - tk1 + d32 = tk3 - tk2 + d42 = tk4 - tk2 + + dt1 = t_param - tk1 + dt2 = t_param - tk2 + d3t = tk3 - t_param + d4t = tk4 - t_param + + d31_d32 = d31 * d32 + d42_d32 = d42 * d32 + + # basis function values + if order == 0: + if sparse is True: + basis_values[0] = (d3t * d3t) / d31_d32 + basis_values[1] = ((dt1 * d3t) / d31_d32) + ((dt2 * d4t) / d42_d32) + basis_values[2] = (dt2 * dt2) / d42_d32 + + else: + basis_values[idx] = (d3t * d3t) / d31_d32 + basis_values[idx + 1] = ((dt1 * d3t) / d31_d32) + ((dt2 * d4t) / d42_d32) + basis_values[idx + 2] = (dt2 * dt2) / d42_d32 + + # basis function derivatives + elif order == 1: + if sparse is True: + basis_values[0] = -2 * d3t / d31_d32 + basis_values[1] = (d3t - dt1) / d31_d32 + (d4t - dt2) / d42_d32 + basis_values[2] = 2 * dt2 / d42_d32 + else: + basis_values[idx] = -2*d3t / d31_d32 + basis_values[idx + 1] = (d3t - dt1) / d31_d32 + (d4t - dt2) / d42_d32 + basis_values[idx + 2] = 2 * dt2 / d42_d32 + + return basis_values, idx + + +def test_spline_base_vec(knots=np.array((0.0, 0.0, 0.0, 1/3.0, 2/3.0, 1.0, 1.0, 1.0)), sparse=False): + """ + Test optimized version of spline base function + :param knots: numpy array of knots + :param sparse: is sparse matrix used + :return: + """ + + num = 100 + n_basf = len(knots) - 3 + y_coords = {} + for k in range(0, n_basf): + temp = {} + for i in range(0, num+1): + t_param = min(knots) + max(knots) * i / float(num) + if sparse is True: + temp[i] = spline_base_vec(knots, t_param, 0, sparse)[0].toarray()[0] + else: + temp[i] = spline_base_vec(knots, t_param, 0, sparse)[0] + y_coords[k] = temp + + diff_x = (max(knots) - min(knots)) / num + x_coord = [min(knots) + diff_x*i for i in range(0, num+1)] + + for temp in y_coords.values(): + plt.plot(x_coord, temp.values()) + plt.show() + + + + +def spline_base(knot_vec, basis_fnc_idx, t_param): + """ + Evaluate a second order bases function in 't_param'. + This function compute normalized blending function aka base function of B-Spline curve or surface. + This function implement some optimization. + :param knot_vec: knot vector + :param basis_fnc_idx: index of basis function + :param t_param: parameter t in interval <0, 1> + :return: value of basis function + """ + + # When basis function has zero value at given interval, then return 0 + if t_param < knot_vec[basis_fnc_idx] or knot_vec[basis_fnc_idx+3] < t_param: + return 0.0 + + try: + value = SB_CACHE[(tuple(knot_vec), basis_fnc_idx, t_param)] + except KeyError: + try: + kvn = KVN_CACHE[tuple(knot_vec)] + except KeyError: + knot_vec_len = len(knot_vec) + kvn = [0] * knot_vec_len + i = 0 + while i < knot_vec_len - 1: + if knot_vec[i] - knot_vec[i+1] != 0: + kvn[i] = 1.0 + i += 1 + KVN_CACHE[tuple(knot_vec)] = kvn + tks = [knot_vec[basis_fnc_idx + k] for k in range(0, 4)] + + value = 0.0 + if knot_vec[basis_fnc_idx] <= t_param <= knot_vec[basis_fnc_idx+1] and kvn[basis_fnc_idx] != 0: + value = (t_param - tks[0])**2 / ((tks[2] - tks[0]) * (tks[1] - tks[0])) + elif knot_vec[basis_fnc_idx+1] <= t_param <= knot_vec[basis_fnc_idx+2] and kvn[basis_fnc_idx+1] != 0: + value = ((t_param - tks[0]) * (tks[2] - t_param)) / ((tks[2] - tks[0]) * (tks[2] - tks[1])) + \ + ((t_param - tks[1]) * (tks[3] - t_param)) / ((tks[3] - tks[1]) * (tks[2] - tks[1])) + elif knot_vec[basis_fnc_idx+2] <= t_param <= knot_vec[basis_fnc_idx+3] and kvn[basis_fnc_idx+2] != 0: + value = (tks[3] - t_param)**2 / ((tks[3] - tks[1]) * (tks[3] - tks[2])) + SB_CACHE[(tuple(knot_vec), basis_fnc_idx, t_param)] = value + + return value + + +KNOT_VEC_CACHE = {} + + +def spline_surface(poles, u_param, v_param, u_knots, v_knots, u_mults, v_mults): + """ + Compute coordinate of one point at B-Surface (u and v degree is 2) + :param poles: matrix of "poles" + :param u_param: X coordinate in range <0, 1> + :param v_param: Y coordinate in range <0, 1> + :param u_knots: list of u knots + :param v_knots: list of v knots + :param u_mults: list of u multiplicities + :param v_mults: list of v multiplicities + :return: tuple of (x, y, z) coordinate of B-Spline surface + """ + + # "Decompress" knot vectors using multiplicities + # e.g + # u_knots: (0.0, 0.5, 1.0) u_mults: (3, 1, 3) will be converted to + # _u_knot: (0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0) + _u_knots = [] + _v_knots = [] + try: + _u_knots = KNOT_VEC_CACHE[(u_knots, u_mults)] + except KeyError: + for idx, mult in enumerate(u_mults): + _u_knots.extend([u_knots[idx]] * mult) + KNOT_VEC_CACHE[(u_knots, u_mults)] = _u_knots + try: + _v_knots = KNOT_VEC_CACHE[(v_knots, v_mults)] + except KeyError: + for idx, mult in enumerate(v_mults): + _v_knots.extend([v_knots[idx]] * mult) + KNOT_VEC_CACHE[(v_knots, v_mults)] = _v_knots + + u_n_basf = len(_u_knots) - 3 + v_n_basf = len(_v_knots) - 3 + + uf_mat = [0.0] * u_n_basf + vf_mat = [0.0] * v_n_basf + + # Pre-compute base values of functions + for k in range(0, u_n_basf): + uf_mat[k] = spline_base(_u_knots, k, u_param) + for k in range(0, v_n_basf): + vf_mat[k] = spline_base(_v_knots, k, v_param) + + x_coord, y_coord, z_coord = 0.0, 0.0, 0.0 + + # Compute point at B-Spline surface + for i in range(0, u_n_basf): + for j in range(0, v_n_basf): + base_i_j = uf_mat[i] * vf_mat[j] + x_coord += poles[i][j][0] * base_i_j + y_coord += poles[i][j][1] * base_i_j + z_coord += poles[i][j][2] * base_i_j + + return x_coord, y_coord, z_coord + + + + + +def transform_points(quad, terrain_data): + """ + Function computes corresponding (u,v) for (x,y) + :param terrain_data: matrix of 3D terrain data, N rows, 3 cols + :param quad: points defining quadrangle area (array) 2 rows 4 cols, + :return: transformed and cropped terrain points + """ + + # if quad points are given counter clock wise, this is outer normal in XY plane + mat_n = np.empty_like(quad) + + mat_n[:, 0] = quad[:, 0] - quad[:, 3] + nt = mat_n[0, 0] + mat_n[0, 0] = -mat_n[1, 0] + mat_n[1, 0] = nt + mat_n[:, 0] = mat_n[:, 0] / la.norm(mat_n[:, 0]) + + for i in range(1, 4): + mat_n[:, i] = quad[:, i] - quad[:, i-1] + nt = mat_n[0, i] + mat_n[0, i] = -mat_n[1, i] + mat_n[1, i] = nt + mat_n[:, i] = mat_n[:, i] / la.norm(mat_n[:, i]) + + terrain_len = len(terrain_data) + + # Compute local coordinates and drop all points outside quadraangle + param_terrain = np.empty_like(terrain_data) + # indexing + d0 = (terrain_data[:, 0:2] - quad[:, 0].transpose()) + d2 = (terrain_data[:, 0:2] - quad[:, 2].transpose()) + d3 = (terrain_data[:, 0:2] - quad[:, 3].transpose()) + d0_n0 = d0 * mat_n[:, 0] + d0_n1 = d0 * mat_n[:, 1] + d2_n2 = d2 * mat_n[:, 2] + d3_n3 = d3 * mat_n[:, 3] + u = np.divide(d0_n0, d0_n0 + d2_n2) + v = np.divide(d0_n1, d0_n1 + d3_n3) + + h = -1 + for j in range(0,terrain_len): + if (u[j] >= 0.0) and (u[j] <= 1.0) and (v[j] >= 0.0) and (v[j] <= 1.0): + h += 1 + param_terrain[h, 0] = u[j] + param_terrain[h, 1] = v[j] + param_terrain[h, 2] = terrain_data[j, 2] + + param_terrain.resize(h+1, 3) + + uv = np.reshape(param_terrain[:, 0:2], 2*h+2).transpose() + + a = quad[:, 3] - quad[:, 2] + b = quad[:, 0] - quad[:, 1] + c = quad[:, 1] - quad[:, 2] + d = quad[:, 0] - quad[:, 3] + + ldiag = np.zeros([2 * h + 1, 1]) + diag = np.zeros([2 * h + 2, 1]) + udiag = np.zeros([2 * h + 1, 1]) + + # fixed point Newton iteration + for i in range(0, 1): # 1->5 + for j in range(0, h+1): + mat_j = compute_jacobi(uv[2 * j, 0], uv[2 * j + 1, 0], a, b, c, d, -1) + ldiag[2 * j, 0] = mat_j[1, 0] + diag[2 * j, 0] = mat_j[0, 0] + diag[2 * j + 1, 0] = mat_j[1, 1] + udiag[2 * j, 0] = mat_j[0, 1] + + mat_jg = scipy.sparse.diags([ldiag[:, 0], diag[:, 0], udiag[:, 0]], [-1, 0, 1], format="csr") + uv = uv - mat_jg.dot(uv) + + uv = uv.reshape([h + 1, 2]) + + # Tresholding of the refined coordinates + for j in range(0, h+1): + if uv[j, 0] < 0: + uv[:, 0] = 0 + elif uv[j, 0] > 1: + uv[j, 0] = 1 + elif uv[j, 1] < 0: + uv[:, 1] = 0 + elif uv[j, 1] > 1: + uv[j, 1] = 1 + + param_terrain[:, 0:2] = uv + + return param_terrain + + diff --git a/src/bspline_plot.py b/src/bspline_plot.py new file mode 100644 index 0000000..e6b888c --- /dev/null +++ b/src/bspline_plot.py @@ -0,0 +1,84 @@ +""" +Functions to plot Bspline curves and surfaces. +""" + +import matplotlib.pyplot as plt +from matplotlib import cm +import numpy as np + + + +def plot_curve_2d(curve, n_points=100, **kwargs): + """ + Plot a 2d Bspline curve. + :param curve: Curve t -> x,y + :param n_points: Number of evaluated points. + :param: kwargs: Additional parameters passed to the mtplotlib plot command. + :return: Plot object. + """ + + basis = curve.basis + t_coord = np.linspace(basis.domain[0], basis.domain[1], n_points) + + coords = [curve.eval(t) for t in t_coord] + x_coord, y_coord = zip(*coords) + return plt.plot(x_coord, y_coord, **kwargs) + + +def plot_curve_poles_2d(curve, **kwargs): + """ + Plot poles of the B-spline curve. + :param curve: Curve t -> x,y + :param: kwargs: Additional parameters passed to the mtplotlib plot command. + :return: Plot object. + """ + x_poles, y_poles = curve.poles.T[0:2, :] # remove weights + return plt.plot(x_poles, y_poles, 'bo', color='red', **kwargs) + + +def plot_surface_3d(surface, fig_ax, n_points=(100, 100), **kwargs): + """ + Plot a surface in 3d. + Usage: + from mpl_toolkits.mplot3d import Axes3D + import matplotlib.pyplot as plt + fig = plt.figure() + ax = fig.gca(projection='3d') + plot_surface_3d(surface, ax) + plt.show() + + :param surface: Parametric surface in 3d. + :param fig_ax: Axes object: + :param n_points: (nu, nv), nu*nv - number of evaluation point + :param kwargs: surface_plot additional options + :return: The plot object. + """ + + u_basis, v_basis = surface.u_basis, surface.v_basis + + u_coord = np.linspace(u_basis.domain[0], u_basis.domain[1], n_points[0]) + v_coord = np.linspace(v_basis.domain[0], v_basis.domain[1], n_points[1]) + + U, V = np.meshgrid(u_coord, v_coord) + points = np.vstack( [U.ravel(), V.ravel()] ) + + xyz = np.array([surface.eval(point[0], point[1]) for point in points.T ]) + X, Y, Z = xyz.T + + X = X.reshape(U.shape) + Y = Y.reshape(U.shape) + Z = Z.reshape(U.shape) + + # Plot the surface. + return fig_ax.plot_surface(X, Y, Z, **kwargs) + + +def plot_surface_poles_3d(surface, fig_ax, **kwargs): + """ + Plot poles of the B-spline curve. + :param curve: Curve t -> x,y + :param: kwargs: Additional parameters passed to the mtplotlib plot command. + :return: Plot object. + """ + x_poles, y_poles, z_poles = surface.poles[:, :, 0:3].reshape(-1, 3).T # remove weights and flatten nu, nv + return fig_ax.scatter(x_poles, y_poles, z_poles, color='red', **kwargs) diff --git a/tests/test_bspline.py b/tests/test_bspline.py new file mode 100644 index 0000000..4d20bb0 --- /dev/null +++ b/tests/test_bspline.py @@ -0,0 +1,133 @@ +import pytest +import bspline as bs +import numpy as np +import math +import bspline_plot as bs_plot +import matplotlib.pyplot as plt + + +class TestSplineBasis: + def test_find_knot_interval(self): + eq_basis = bs.SplineBasis.make_equidistant(2, 100) + assert eq_basis.find_knot_interval(0.0) == 0 + assert eq_basis.find_knot_interval(0.001) == 0 + assert eq_basis.find_knot_interval(0.01) == 0 + assert eq_basis.find_knot_interval(0.011) == 1 + assert eq_basis.find_knot_interval(0.5001) == 50 + assert eq_basis.find_knot_interval(1.0 - 0.011) == 98 + assert eq_basis.find_knot_interval(1.0 - 0.01) == 98 + assert eq_basis.find_knot_interval(1.0 - 0.001) == 99 + assert eq_basis.find_knot_interval(1.0) == 99 + + def plot_basis(self, degree): + + eq_basis = bs.SplineBasis.make_equidistant(degree, 4) + + n_points = 401 + dx = (eq_basis.domain[1] - eq_basis.domain[0]) / (n_points -1) + x_coord = [eq_basis.domain[0] + dx * i for i in range(n_points)] + + for i_base in range(eq_basis.size): + y_coord = [ eq_basis.eval(i_base, x) for x in x_coord ] + plt.plot(x_coord, y_coord) + + plt.show() + + + def test_eval(self): + #self.plot_basis(0) + #self.plot_basis(1) + #self.plot_basis(2) + #self.plot_basis(3) + + eq_basis = bs.SplineBasis.make_equidistant(0, 2) + assert eq_basis.eval(0, 0.0) == 1.0 + assert eq_basis.eval(1, 0.0) == 0.0 + assert eq_basis.eval(0, 0.5) == 0.0 + assert eq_basis.eval(1, 0.5) == 1.0 + assert eq_basis.eval(1, 1.0) == 1.0 + + eq_basis = bs.SplineBasis.make_equidistant(1, 4) + assert eq_basis.eval(0, 0.0) == 1.0 + assert eq_basis.eval(1, 0.0) == 0.0 + assert eq_basis.eval(2, 0.0) == 0.0 + assert eq_basis.eval(3, 0.0) == 0.0 + assert eq_basis.eval(4, 0.0) == 0.0 + + assert eq_basis.eval(0, 0.125) == 0.5 + assert eq_basis.eval(1, 0.125) == 0.5 + assert eq_basis.eval(2, 1.0) == 0.0 + + +class TestCurve: + + def plot_4p(self): + degree = 2 + poles = [ [0., 0.], [1.0, 0.5], [2., -2.], [3., 1.] ] + basis = bs.SplineBasis.make_equidistant(degree, 2) + curve = bs.Curve(basis, poles) + + bs_plot.plot_curve_2d(curve) + bs_plot.plot_curve_poles_2d(curve) + plt.show() + + def test_evaluate(self): + #self.plot_4p() + pass + + +class TestSurface: + + + def make_function_grid(self, fn, nu, nv): + X_grid = np.linspace(0, 1.0, nu) + Y_grid = np.linspace(0, 1.0, nv) + Y, X = np.meshgrid(Y_grid, X_grid) + + points_uv = np.stack([X.ravel(), Y.ravel()], 1) + Z = np.apply_along_axis(fn, 1, points_uv) + points = np.stack([X.ravel(), Y.ravel(), Z], 1) + + return points.reshape( (nu, nv, 3) ) + + def plot_extrude(self): + fig = plt.figure() + ax = fig.gca(projection='3d') + + # curve extruded to surface + poles_yz = [[0., 0.], [1.0, 0.5], [2., -2.], [3., 1.]] + poles_x = [0, 1, 2] + poles = [ [ [x] + yz for yz in poles_yz ] for x in poles_x ] + u_basis = bs.SplineBasis.make_equidistant(2, 1) + v_basis = bs.SplineBasis.make_equidistant(2, 2) + surface_extrude = bs.Surface( (u_basis, v_basis), poles) + bs_plot.plot_surface_3d(surface_extrude, ax) + bs_plot.plot_surface_poles_3d(surface_extrude, ax) + plt.show() + + def plot_function(self): + # function surface + def function(x): + return math.sin(x[0]) * math.cos(x[1]) + + fig = plt.figure() + ax = fig.gca(projection='3d') + + poles = self.make_function_grid(function, 4, 5) + u_basis = bs.SplineBasis.make_equidistant(2, 2) + v_basis = bs.SplineBasis.make_equidistant(2, 3) + surface_func = bs.Surface( (u_basis, v_basis), poles) + bs_plot.plot_surface_3d(surface_func, ax) + bs_plot.plot_surface_poles_3d(surface_func, ax) + + plt.show() + + def test_evaluate(self): + from mpl_toolkits.mplot3d import Axes3D + import matplotlib.pyplot as plt + + self.plot_extrude() + self.plot_function() + + + From a6ec4e8e4a63798d17ace72c469e985f4a14cc58 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Sat, 16 Sep 2017 11:16:43 +0200 Subject: [PATCH 15/36] Tested Z-surface --- src/bspline.py | 401 ++++++++++-------------------------------- src/bspline_plot.py | 3 + tests/test_bspline.py | 80 +++++++-- 3 files changed, 168 insertions(+), 316 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index 4aa0271..fad4e2b 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -3,9 +3,12 @@ These classes provide just basic functionality: - storing the data - evaluation of XYZ for UV -- derivatives In future: +- evaluation and xy<->uv functions accepting np.arrays, - serialization and deserialization using JSONdata - must make it an installable module +- use de Boor algorithm for evaluation of curves and surfaces +- evaluation of derivatives +- implement degree increasing and knot insertion """ """ @@ -109,6 +112,7 @@ def make_equidistant(cls, degree, n_intervals, knot_range=[0.0, 1.0]): knots[-degree - 1:] = knot_range[1] return cls(degree, knots) + @classmethod def make_from_packed_knots(cls, degree, knots): full_knots = [ q for q, mult in knots for i in range(mult) ] @@ -136,6 +140,7 @@ def __init__(self, degree, knots): self.domain_size = self.domain[1] - self.domain[0] # Number of basis functions. + def pack_knots(self): last, mult = self.knots[0], 0 packed_knots = [] @@ -147,6 +152,7 @@ def pack_knots(self): last, mult = q, 1 return packed_knots + def find_knot_interval(self, t): """ Find the first non-empty knot interval containing the value 't'. @@ -241,6 +247,17 @@ def eval(self, i_base, t): return 1.0 return self._basis(self.degree, i_base, t) + def make_linear_poles(self): + """ + Return poles of basis functions to get a f(x) = x. + :return: + """ + poles= [ 0.0 ] + for i in range(self.size-1): + pp = poles[-1] + (self.knots[i + self.degree + 1] - self.knots[i + 1]) / float(self.degree) + poles.append(pp) + return poles + class Curve: @@ -266,14 +283,14 @@ def make_raw(cls, poles, knots, rational=False, degree=2): """ def __init__(self, basis, poles, rational = False): self.basis = basis - dim = len(poles[0]) - rational - check_matrix(poles, [self.basis.size, dim + rational], scalar_types ) + self.dim = len(poles[0]) - rational + check_matrix(poles, [self.basis.size, self.dim + rational], scalar_types ) self.poles=np.array(poles) # N x D self.rational=rational if rational: - self._weights = poles[:, dim] - self._poles = (poles[:, 0:dim].T * self._weights ).T + self._weights = poles[:, self.dim] + self._poles = (poles[:, 0:self.dim].T * self._weights ).T def eval(self, t): @@ -332,13 +349,13 @@ def __init__(self, basis, poles, rational=False): self.u_basis, self.v_basis = basis self.rational = rational - dim = len(poles[0][0]) - rational - check_matrix(poles, [self.u_basis.size, self.v_basis.size, dim + rational], scalar_types ) + self.dim = len(poles[0][0]) - rational + check_matrix(poles, [self.u_basis.size, self.v_basis.size, self.dim + rational], scalar_types ) self.poles=np.array(poles) - assert self.poles.shape == (self.u_basis.size, self.v_basis.size, dim + rational) + assert self.poles.shape == (self.u_basis.size, self.v_basis.size, self.dim + rational) if rational: - self._weights = poles[:, :, dim] - self._poles = (poles[:,:,0:dim].T * self._weights.T ).T + self._weights = poles[:, :, self.dim] + self._poles = (poles[:,:,0:self.dim].T * self._weights.T ).T def eval(self, u, v): iu = self.u_basis.find_knot_interval(u) @@ -372,329 +389,105 @@ class Z_Surface: - We need conversion to full 3D surface for the BREP output - Optimization: simplified Bspline evaluation just for the singel coordinate """ + def __init__(self, xy_quad, z_surface): + """ + Construct a surface given by the 1d surface for the Z coordinate and XY quadrilateral + for the bilinear UV -> XY mapping. + :param xy_quad: np array N x 2 + Four or three points, determining bilinear or linear mapping, respectively. + Four points giving XY coordinates for the uv corners: (0,0), (0,1), (1,0), (1,1) + Three points giving XY coordinates for the uv corners: (0,0), (0,1), (1,0) + :param z_surface: !D Surface object. + """ + assert z_surface.dim == 1 + self.z_surface = z_surface + self.u_basis = z_surface.u_basis + self.v_basis = z_surface.v_basis + self.dim = 3 + + if len(xy_quad) == 3: + # linear case + self.xy_shift = xy_quad[0] + v_vec = xy_quad[1] - xy_quad[0] + u_vec = xy_quad[2] - xy_quad[0] + self.mat_uv_to_xy = np.column_stack((u_vec, v_vec)) + self.mat_xy_to_uv = la.inv(self.mat_uv_to_xy) + + self.xy_to_uv = self._linear_xy_to_uv + self.uv_to_xy = self._linear_uv_to_xy + + elif len(xy_quad) == 4: + # bilinear case + self.quad = xy_quad + + self.xy_to_uv = self._bilinear_xy_to_uv + self.uv_to_xy = self._bilinear_uv_to_xy - def eval_xy(self, xy_points): - pass - - - - - - - - - - - - - - - - - - - - - - - - - - -# Cache used of knot vector (computation of differences) -KVN_CACHE = {} -SB_CACHE = {} - - - -def spline_base_vec(knot_vec, t_param, order, sparse=False): - """ - This function compute normalized blending function aka base function of B-Spline curve or surface. - :param knot_vec: - :param t_param: - :param order: (0: function value, 1: derivative function value) - :param sparse: - :return: - """ - + else: + assert False, "Three or four points must be given." - idx = find_index(knot_vec, t_param) - n_basf = len(knot_vec) - 3 + def make_full_surface(self): + """ + Return representation of the surface by the 3d Surface object. + Compute redundant XY poles. + :return: Surface. + """ + basis = (self.z_surface.u_basis, self.z_surface.v_basis) - # Create sparse matrix - if sparse is True: - basis_values = np.zeros(3) - else: - basis_values = np.zeros(n_basf) + u = basis[0].make_linear_poles() + v = basis[1].make_linear_poles() + V, U = np.meshgrid(v,u) + uv_poles = np.stack([U, V], axis=2) + xy_poles = np.apply_along_axis(lambda x: self.uv_to_xy(x[0], x[1]), 2, uv_poles) + poles = np.concatenate( (xy_poles, self.z_surface.poles), axis = 2 ) - tk1 = knot_vec[idx + 1] - tk2 = knot_vec[idx + 2] - tk3 = knot_vec[idx + 3] - tk4 = knot_vec[idx + 4] + return Surface(basis, poles) - d31 = tk3 - tk1 - d32 = tk3 - tk2 - d42 = tk4 - tk2 + def _linear_uv_to_xy(self, u, v): + #assert uv_points.shape[0] == 2, "Size: {}".format(uv_points.shape) + uv_points = np.array([u, v]) + return self.mat_uv_to_xy.dot(uv_points) + self.xy_shift - dt1 = t_param - tk1 - dt2 = t_param - tk2 - d3t = tk3 - t_param - d4t = tk4 - t_param - d31_d32 = d31 * d32 - d42_d32 = d42 * d32 + def _bilinear_uv_to_xy(self, u, v): + weights = np.array([ (1-u)*(1-v), (1-u)*v, u*(1-v), u*v ]) + return self.quad.T.dot( weights ) - # basis function values - if order == 0: - if sparse is True: - basis_values[0] = (d3t * d3t) / d31_d32 - basis_values[1] = ((dt1 * d3t) / d31_d32) + ((dt2 * d4t) / d42_d32) - basis_values[2] = (dt2 * dt2) / d42_d32 + def _linear_xy_to_uv(self, x, y): + # assert xy_points.shape[0] == 2 + xy_points = np.array([x,y]) + return self.mat_xy_to_uv.dot((xy_points - self.xy_shift)) - else: - basis_values[idx] = (d3t * d3t) / d31_d32 - basis_values[idx + 1] = ((dt1 * d3t) / d31_d32) + ((dt2 * d4t) / d42_d32) - basis_values[idx + 2] = (dt2 * dt2) / d42_d32 - - # basis function derivatives - elif order == 1: - if sparse is True: - basis_values[0] = -2 * d3t / d31_d32 - basis_values[1] = (d3t - dt1) / d31_d32 + (d4t - dt2) / d42_d32 - basis_values[2] = 2 * dt2 / d42_d32 - else: - basis_values[idx] = -2*d3t / d31_d32 - basis_values[idx + 1] = (d3t - dt1) / d31_d32 + (d4t - dt2) / d42_d32 - basis_values[idx + 2] = 2 * dt2 / d42_d32 - return basis_values, idx + def _bilinear_xy_to_uv(self, x, y): + assert False, "Not implemented yet." -def test_spline_base_vec(knots=np.array((0.0, 0.0, 0.0, 1/3.0, 2/3.0, 1.0, 1.0, 1.0)), sparse=False): - """ - Test optimized version of spline base function - :param knots: numpy array of knots - :param sparse: is sparse matrix used - :return: - """ - - num = 100 - n_basf = len(knots) - 3 - y_coords = {} - for k in range(0, n_basf): - temp = {} - for i in range(0, num+1): - t_param = min(knots) + max(knots) * i / float(num) - if sparse is True: - temp[i] = spline_base_vec(knots, t_param, 0, sparse)[0].toarray()[0] - else: - temp[i] = spline_base_vec(knots, t_param, 0, sparse)[0] - y_coords[k] = temp - - diff_x = (max(knots) - min(knots)) / num - x_coord = [min(knots) + diff_x*i for i in range(0, num+1)] - - for temp in y_coords.values(): - plt.plot(x_coord, temp.values()) - plt.show() - + def eval(self, u, v): + z = self.z_surface.eval(u, v) + x, y = self.uv_to_xy(u, v) + return np.array( [x, y, z] ) + def eval_xy(self, x, y): + u, v = self.xy_to_uv(x, y) + z = self.z_surface.eval(u, v) + return np.array([x, y, z]) -def spline_base(knot_vec, basis_fnc_idx, t_param): - """ - Evaluate a second order bases function in 't_param'. - This function compute normalized blending function aka base function of B-Spline curve or surface. - This function implement some optimization. - :param knot_vec: knot vector - :param basis_fnc_idx: index of basis function - :param t_param: parameter t in interval <0, 1> - :return: value of basis function - """ - # When basis function has zero value at given interval, then return 0 - if t_param < knot_vec[basis_fnc_idx] or knot_vec[basis_fnc_idx+3] < t_param: - return 0.0 - try: - value = SB_CACHE[(tuple(knot_vec), basis_fnc_idx, t_param)] - except KeyError: - try: - kvn = KVN_CACHE[tuple(knot_vec)] - except KeyError: - knot_vec_len = len(knot_vec) - kvn = [0] * knot_vec_len - i = 0 - while i < knot_vec_len - 1: - if knot_vec[i] - knot_vec[i+1] != 0: - kvn[i] = 1.0 - i += 1 - KVN_CACHE[tuple(knot_vec)] = kvn - tks = [knot_vec[basis_fnc_idx + k] for k in range(0, 4)] - - value = 0.0 - if knot_vec[basis_fnc_idx] <= t_param <= knot_vec[basis_fnc_idx+1] and kvn[basis_fnc_idx] != 0: - value = (t_param - tks[0])**2 / ((tks[2] - tks[0]) * (tks[1] - tks[0])) - elif knot_vec[basis_fnc_idx+1] <= t_param <= knot_vec[basis_fnc_idx+2] and kvn[basis_fnc_idx+1] != 0: - value = ((t_param - tks[0]) * (tks[2] - t_param)) / ((tks[2] - tks[0]) * (tks[2] - tks[1])) + \ - ((t_param - tks[1]) * (tks[3] - t_param)) / ((tks[3] - tks[1]) * (tks[2] - tks[1])) - elif knot_vec[basis_fnc_idx+2] <= t_param <= knot_vec[basis_fnc_idx+3] and kvn[basis_fnc_idx+2] != 0: - value = (tks[3] - t_param)**2 / ((tks[3] - tks[1]) * (tks[3] - tks[2])) - SB_CACHE[(tuple(knot_vec), basis_fnc_idx, t_param)] = value - - return value - - -KNOT_VEC_CACHE = {} - - -def spline_surface(poles, u_param, v_param, u_knots, v_knots, u_mults, v_mults): - """ - Compute coordinate of one point at B-Surface (u and v degree is 2) - :param poles: matrix of "poles" - :param u_param: X coordinate in range <0, 1> - :param v_param: Y coordinate in range <0, 1> - :param u_knots: list of u knots - :param v_knots: list of v knots - :param u_mults: list of u multiplicities - :param v_mults: list of v multiplicities - :return: tuple of (x, y, z) coordinate of B-Spline surface - """ - # "Decompress" knot vectors using multiplicities - # e.g - # u_knots: (0.0, 0.5, 1.0) u_mults: (3, 1, 3) will be converted to - # _u_knot: (0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0) - _u_knots = [] - _v_knots = [] - try: - _u_knots = KNOT_VEC_CACHE[(u_knots, u_mults)] - except KeyError: - for idx, mult in enumerate(u_mults): - _u_knots.extend([u_knots[idx]] * mult) - KNOT_VEC_CACHE[(u_knots, u_mults)] = _u_knots - try: - _v_knots = KNOT_VEC_CACHE[(v_knots, v_mults)] - except KeyError: - for idx, mult in enumerate(v_mults): - _v_knots.extend([v_knots[idx]] * mult) - KNOT_VEC_CACHE[(v_knots, v_mults)] = _v_knots - u_n_basf = len(_u_knots) - 3 - v_n_basf = len(_v_knots) - 3 - uf_mat = [0.0] * u_n_basf - vf_mat = [0.0] * v_n_basf - # Pre-compute base values of functions - for k in range(0, u_n_basf): - uf_mat[k] = spline_base(_u_knots, k, u_param) - for k in range(0, v_n_basf): - vf_mat[k] = spline_base(_v_knots, k, v_param) - x_coord, y_coord, z_coord = 0.0, 0.0, 0.0 - # Compute point at B-Spline surface - for i in range(0, u_n_basf): - for j in range(0, v_n_basf): - base_i_j = uf_mat[i] * vf_mat[j] - x_coord += poles[i][j][0] * base_i_j - y_coord += poles[i][j][1] * base_i_j - z_coord += poles[i][j][2] * base_i_j - return x_coord, y_coord, z_coord -def transform_points(quad, terrain_data): - """ - Function computes corresponding (u,v) for (x,y) - :param terrain_data: matrix of 3D terrain data, N rows, 3 cols - :param quad: points defining quadrangle area (array) 2 rows 4 cols, - :return: transformed and cropped terrain points - """ - # if quad points are given counter clock wise, this is outer normal in XY plane - mat_n = np.empty_like(quad) - - mat_n[:, 0] = quad[:, 0] - quad[:, 3] - nt = mat_n[0, 0] - mat_n[0, 0] = -mat_n[1, 0] - mat_n[1, 0] = nt - mat_n[:, 0] = mat_n[:, 0] / la.norm(mat_n[:, 0]) - - for i in range(1, 4): - mat_n[:, i] = quad[:, i] - quad[:, i-1] - nt = mat_n[0, i] - mat_n[0, i] = -mat_n[1, i] - mat_n[1, i] = nt - mat_n[:, i] = mat_n[:, i] / la.norm(mat_n[:, i]) - - terrain_len = len(terrain_data) - - # Compute local coordinates and drop all points outside quadraangle - param_terrain = np.empty_like(terrain_data) - # indexing - d0 = (terrain_data[:, 0:2] - quad[:, 0].transpose()) - d2 = (terrain_data[:, 0:2] - quad[:, 2].transpose()) - d3 = (terrain_data[:, 0:2] - quad[:, 3].transpose()) - d0_n0 = d0 * mat_n[:, 0] - d0_n1 = d0 * mat_n[:, 1] - d2_n2 = d2 * mat_n[:, 2] - d3_n3 = d3 * mat_n[:, 3] - u = np.divide(d0_n0, d0_n0 + d2_n2) - v = np.divide(d0_n1, d0_n1 + d3_n3) - - h = -1 - for j in range(0,terrain_len): - if (u[j] >= 0.0) and (u[j] <= 1.0) and (v[j] >= 0.0) and (v[j] <= 1.0): - h += 1 - param_terrain[h, 0] = u[j] - param_terrain[h, 1] = v[j] - param_terrain[h, 2] = terrain_data[j, 2] - - param_terrain.resize(h+1, 3) - - uv = np.reshape(param_terrain[:, 0:2], 2*h+2).transpose() - - a = quad[:, 3] - quad[:, 2] - b = quad[:, 0] - quad[:, 1] - c = quad[:, 1] - quad[:, 2] - d = quad[:, 0] - quad[:, 3] - - ldiag = np.zeros([2 * h + 1, 1]) - diag = np.zeros([2 * h + 2, 1]) - udiag = np.zeros([2 * h + 1, 1]) - - # fixed point Newton iteration - for i in range(0, 1): # 1->5 - for j in range(0, h+1): - mat_j = compute_jacobi(uv[2 * j, 0], uv[2 * j + 1, 0], a, b, c, d, -1) - ldiag[2 * j, 0] = mat_j[1, 0] - diag[2 * j, 0] = mat_j[0, 0] - diag[2 * j + 1, 0] = mat_j[1, 1] - udiag[2 * j, 0] = mat_j[0, 1] - - mat_jg = scipy.sparse.diags([ldiag[:, 0], diag[:, 0], udiag[:, 0]], [-1, 0, 1], format="csr") - uv = uv - mat_jg.dot(uv) - - uv = uv.reshape([h + 1, 2]) - - # Tresholding of the refined coordinates - for j in range(0, h+1): - if uv[j, 0] < 0: - uv[:, 0] = 0 - elif uv[j, 0] > 1: - uv[j, 0] = 1 - elif uv[j, 1] < 0: - uv[:, 1] = 0 - elif uv[j, 1] > 1: - uv[j, 1] = 1 - - param_terrain[:, 0:2] = uv - - return param_terrain diff --git a/src/bspline_plot.py b/src/bspline_plot.py index e6b888c..74a4a65 100644 --- a/src/bspline_plot.py +++ b/src/bspline_plot.py @@ -82,3 +82,6 @@ def plot_surface_poles_3d(surface, fig_ax, **kwargs): """ x_poles, y_poles, z_poles = surface.poles[:, :, 0:3].reshape(-1, 3).T # remove weights and flatten nu, nv return fig_ax.scatter(x_poles, y_poles, z_poles, color='red', **kwargs) + + + diff --git a/tests/test_bspline.py b/tests/test_bspline.py index 4d20bb0..0cf26ec 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -59,6 +59,18 @@ def test_eval(self): assert eq_basis.eval(2, 1.0) == 0.0 + def test_linear_poles(self): + eq_basis = bs.SplineBasis.make_equidistant(2, 4) + poles = eq_basis.make_linear_poles() + + t_vec = np.linspace(0.0, 1.0, 21) + x_vec = [] + for t in t_vec: + b_vals = np.array([ eq_basis.eval(i, t) for i in range(eq_basis.size) ]) + x = np.dot(b_vals, poles) + assert np.abs( x - t ) < 1e-15 + + class TestCurve: def plot_4p(self): @@ -75,20 +87,29 @@ def test_evaluate(self): #self.plot_4p() pass + # TODO: test rational curves, e.g. circle + + + + -class TestSurface: - def make_function_grid(self, fn, nu, nv): - X_grid = np.linspace(0, 1.0, nu) - Y_grid = np.linspace(0, 1.0, nv) - Y, X = np.meshgrid(Y_grid, X_grid) +def make_function_grid(fn, nu, nv): + X_grid = np.linspace(0, 1.0, nu) + Y_grid = np.linspace(0, 1.0, nv) + Y, X = np.meshgrid(Y_grid, X_grid) - points_uv = np.stack([X.ravel(), Y.ravel()], 1) - Z = np.apply_along_axis(fn, 1, points_uv) - points = np.stack([X.ravel(), Y.ravel(), Z], 1) + points_uv = np.stack([X.ravel(), Y.ravel()], 1) + Z = np.apply_along_axis(fn, 1, points_uv) + points = np.stack([X.ravel(), Y.ravel(), Z], 1) - return points.reshape( (nu, nv, 3) ) + return points.reshape( (nu, nv, 3) ) + + + + +class TestSurface: def plot_extrude(self): fig = plt.figure() @@ -113,7 +134,7 @@ def function(x): fig = plt.figure() ax = fig.gca(projection='3d') - poles = self.make_function_grid(function, 4, 5) + poles = make_function_grid(function, 4, 5) u_basis = bs.SplineBasis.make_equidistant(2, 2) v_basis = bs.SplineBasis.make_equidistant(2, 3) surface_func = bs.Surface( (u_basis, v_basis), poles) @@ -126,8 +147,43 @@ def test_evaluate(self): from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt - self.plot_extrude() - self.plot_function() + #self.plot_extrude() + #self.plot_function() + + + # TODO: test rational surfaces, e.g. sphere + +class TestZ_Surface: + + + + + def plot_function_uv(self): + # function surface + def function(x): + return math.sin(x[0]*4) * math.cos(x[1]*4) + + fig = plt.figure() + ax = fig.gca(projection='3d') + + poles = make_function_grid(function, 4, 5) + u_basis = bs.SplineBasis.make_equidistant(2, 2) + v_basis = bs.SplineBasis.make_equidistant(2, 3) + surface_func = bs.Surface( (u_basis, v_basis), poles[:,:, [2] ]) + + quad = np.array( [ [0, 0], [0, 0.5], [1, 0.1], [1.1, 1.1] ] ) + #quad = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) + z_surf = bs.Z_Surface(quad, surface_func) + full_surf = z_surf.make_full_surface() + + bs_plot.plot_surface_3d(z_surf, ax) + bs_plot.plot_surface_3d(full_surf, ax, color='red') + #bs_plot.plot_surface_poles_3d(surface_func, ax) + + plt.show() + def test_eval_uv(self): + self.plot_function_uv() + pass \ No newline at end of file From 06ea538107b46604ceb9e8981ddb835cc14752f9 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Sat, 16 Sep 2017 14:39:22 +0200 Subject: [PATCH 16/36] Integrate GridSurface --- src/bspline.py | 265 +++++++++++++++++++++++++++++++++++++++--- src/bspline_plot.py | 4 +- tests/test_bspline.py | 74 +++++++++++- 3 files changed, 318 insertions(+), 25 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index fad4e2b..f075bfb 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -306,7 +306,13 @@ def eval(self, t): else: return np.inner(t_base_vec, self.poles[it: it + dt, :].T) - + def eval_array(self, t_points): + """ + Evaluate in array of t-points. + :param t_points: array N x float + :return: Numpy array N x D, D is dimension of the curve. + """ + return np.array( [ self.eval(t) for t in t_points] ) @@ -378,6 +384,13 @@ def eval(self, u, v): #print("val: {}".format(value.shape)) return np.inner( value, v_base_vec) + def eval_array(self, uv_points): + """ + Evaluate in array of t-points. + :param uv_points: numpy array N x [u, v] + :return: Numpy array N x D, D is dimension of the curve. + """ + return np.array( [ self.eval(u, v) for u, v in uv_points] ) @@ -405,6 +418,9 @@ def __init__(self, xy_quad, z_surface): self.v_basis = z_surface.v_basis self.dim = 3 + self.z_scale = 1.0 + self.z_shift = 0.0 + if len(xy_quad) == 3: # linear case self.xy_shift = xy_quad[0] @@ -437,48 +453,259 @@ def make_full_surface(self): u = basis[0].make_linear_poles() v = basis[1].make_linear_poles() V, U = np.meshgrid(v,u) - uv_poles = np.stack([U, V], axis=2) - xy_poles = np.apply_along_axis(lambda x: self.uv_to_xy(x[0], x[1]), 2, uv_poles) + uv_poles_vec = np.stack([U.ravel(), V.ravel()], axis=1) + xy_poles = self.uv_to_xy(uv_poles_vec).reshape(U.shape[0], U.shape[1], 2) poles = np.concatenate( (xy_poles, self.z_surface.poles), axis = 2 ) return Surface(basis, poles) - def _linear_uv_to_xy(self, u, v): - #assert uv_points.shape[0] == 2, "Size: {}".format(uv_points.shape) - uv_points = np.array([u, v]) - return self.mat_uv_to_xy.dot(uv_points) + self.xy_shift + + def transform(self, xy_mat, z_mat): + """ + Transform the surface by arbitrary XY linear transform and Z linear transform. + :param xy_mat: np array, 2 rows 3 cols, last column is xy shift + :param z_shift: [ z_scale, z_shift] + :return: None + """ + assert xy_mat.shape == (2, 3) + assert z_mat.shape == (2, ) + self.mat_uv_to_xy = xy_mat[0:2,0:2].dot( self.mat_uv_to_xy ) + self.xy_shift = xy_mat[0:2,0:2].dot( self.xy_shift ) + xy_mat[0:2, 2] + self.mat_xy_to_uv = la.inv(self.mat_uv_to_xy) + + self.z_scale *= z_mat[0] + self.z_shift = z_mat[0] * self.z_shift + z_mat[1] + + + + """ + def uv_to_xy(self, uv_points): + + Abstract method. Converts array of uv points to array of xy points. + :param uv_points: numpy array N x [u,v] + :return: numpy array N x [x,y] + """ + def _linear_uv_to_xy(self, uv_points): + assert uv_points.shape[0] == 2, "Size: {}".format(uv_points.shape) + return (self.mat_uv_to_xy.dot(uv_points).T + self.xy_shift).T - def _bilinear_uv_to_xy(self, u, v): - weights = np.array([ (1-u)*(1-v), (1-u)*v, u*(1-v), u*v ]) - return self.quad.T.dot( weights ) + def _bilinear_uv_to_xy(self, uv_points): + xy_list = [] + for u,v in uv_points: + weights = np.array([ (1-u)*(1-v), (1-u)*v, u*(1-v), u*v ]) + xy_list.append( self.quad.T.dot(weights) ) + return np.array(xy_list) - def _linear_xy_to_uv(self, x, y): + """ + def xy_to_uv(self, xy_points): + + Abstract method. Converts array of xy points to array of uv points. + :param xy_points: numpy array N x [x,y] + :return: numpy array N x [u,v] + """ + def _linear_xy_to_uv(self, xy_points): # assert xy_points.shape[0] == 2 - xy_points = np.array([x,y]) - return self.mat_xy_to_uv.dot((xy_points - self.xy_shift)) + assert xy_points.shape[0] == 2 + return self.mat_xy_to_uv.dot((xy_points.T - self.xy_shift).T) - def _bilinear_xy_to_uv(self, x, y): + def _bilinear_xy_to_uv(self, xy_points): assert False, "Not implemented yet." def eval(self, u, v): z = self.z_surface.eval(u, v) - x, y = self.uv_to_xy(u, v) + uv_points = np.array([[u, v]]) + x, y = self.uv_to_xy( uv_points )[0] return np.array( [x, y, z] ) - def eval_xy(self, x, y): - u, v = self.xy_to_uv(x, y) - z = self.z_surface.eval(u, v) - return np.array([x, y, z]) + def eval_array(self, uv_points): + z_points = self.z_surface.eval_array(uv_points) + xy_points = self.uv_to_xy(uv_points) + return np.concatenate( (xy_points, z_points), axis = 1) + def eval_xy_array(self, xy_points): + uv_points = self.xy_to_uv(xy_points) + z_points = self.z_surface.eval_array(uv_points) + return np.concatenate( (xy_points, z_points), axis = 1) + + def z_eval_array(self, uv_points): + z_points = self.z_surface.eval_array(uv_points) + return z_points + + def z_eval_xy_array(self, uv_points): + uv_points = self.xy_to_uv(xy_points) + z_points = self.z_surface.eval_array(uv_points) + return z_points + +class InvalidGridExc(Exception): + pass + + +class GridSurface: + """ + Surface given as bilinear interpolation of a regular grid of points. + TODO: This can be viewed as a degree 1 Z-function B-spline. Make it a common class for any degree?? + """ + step_tolerance = 1e-10 + + def __init__(self): + """ + Initialize point grid from numpy array. + :param grid: NxMx3 numpy array of NxM grid of #D coordinates + """ + self.grid=None + self.mat_xy_to_uv=None + self.mat_uv_to_xy=None + self.shift=None + self.shape = (0,0) + self.uv_step = (0,0) + def load(self, filename): + """ + Load the grid surface from file + :param filename: + :return: + """ + point_seq = np.loadtxt(filename) + assert min(point_seq.shape) > 1 + self.init_from_seq(point_seq.T) + + + def init_from_seq(self, point_seq): + """ + Get 2d transform matrix 2 rows 3 cols to map a grid of XY points to unit square + :param point_seq: numpy array N x 2 + :return: + """ + + assert point_seq.shape[0] == 3 + n_points = point_seq.shape[1] + point_seq_xy = point_seq[0:2,:] + + vtx_00 = point_seq_xy[0:2, 0] + vtx_du = point_seq_xy[0:2, 1] + + u_step = vtx_du - vtx_00 + for i in range(2, n_points): + step = point_seq_xy[:,i] - point_seq_xy[:,i-1] + if la.norm(u_step - step) > self.step_tolerance: + break + + vtx_dv = point_seq_xy[:,i] + v_step = vtx_dv - vtx_00 + + nu = i + nv = int(n_points / nu) + if not n_points == nu*nv: + raise InvalidGridExc("Not a M*N grid.") + + # check total range of the grid + vtx_10 = point_seq_xy[:, nu-1] + vtx_01 = point_seq_xy[:, -nu] + vtx_11 = point_seq_xy[:, -1] + u_range_0 = vtx_10 - vtx_00 + u_range_1 = vtx_11 - vtx_01 + v_range_0 = vtx_01 - vtx_00 + v_range_1 = vtx_11 - vtx_10 + + if not la.norm(u_range_0 - u_range_1) < self.step_tolerance or \ + not la.norm(v_range_0 - v_range_1) < self.step_tolerance: + raise InvalidGridExc("Grid XY envelope is not a parallelogram.") + + u_step = u_range_0 / (nu-1) + v_step = v_range_0 / (nv-1) + + # check regularity of the grid + for i in range(nu*nv): + pred_x = i - 1 + pred_y = i - nu + if i%nu == 0: + pred_x= -1 + if pred_x > 0 and not la.norm(point_seq_xy[:, i] - point_seq_xy[:, pred_x] - u_step) < 2*self.step_tolerance: + raise InvalidGridExc("Irregular grid in X direction, point %d"%i) + if pred_y > 0 and not la.norm(point_seq_xy[:, i] - point_seq_xy[:, pred_y] - v_step) < 2*self.step_tolerance: + raise InvalidGridExc("Irregular grid in Y direction, point %d"%i) + + quad = np.stack([vtx_00, vtx_01, vtx_10], axis = 0) + u_basis = SplineBasis.make_equidistant(1, nu-1) + v_basis = SplineBasis.make_equidistant(1, nv-1) + + self.grid = point_seq[2, :].reshape(nv, nu) + poles_z = np.transpose( point_seq[2, :].reshape(nv, nu, 1), axes = [1, 0, 2] ) + self.z_surface = Z_Surface(quad, Surface((u_basis, v_basis), poles_z) ) + + self.shape = (nu, nv) + self.uv_step = (1.0 / float(nu - 1), 1.0 / float(nv - 1)) + + + def get_xy_envelope(self): + """ + Returns the envelope of the surface domain in XY plane as a quadrilateral polygon. + :return: Polygon as a array of the XY points in conunter clockwise direction. + """ + return self.uv_to_xy(np.array([[0,0], [1,0], [1,1], [0,1]]).T) + + + + def xy_to_uv(self, xy_points): + """ + :param xy_points: numpy matrix 2 rows N cols + :return: matrix of UV coordinates + """ + return self.z_surface.xy_to_uv(xy_points) + + + def uv_to_xy(self, uv_points): + """ + :param xy_points: numpy matrix 2 rows N cols + :return: matrix of UV coordinates + """ + return self.z_surface.uv_to_xy(uv_points) + + + def eval_array(self, uv_points): + return self.z_surface.eval_array(uv_points) + + + def z_eval_xy_array(self, xy_points): + """ + Return np array of z-values. Optimized version. + :param points: np array N x 2 of XY cooridinates + :return: array of Z values + """ + assert xy_points.shape[0] == 2, "Size: {}".format(xy_points.shape) + uv_points = self.z_surface.xy_to_uv(xy_points) + return self.z_eval_array(uv_points) + + + def z_eval_array(self, uv_points): + """ + Return np array of z-values. Optimized version. + :param points: np array N x 2 of XY cooridinates + :return: array of Z values + """ + assert uv_points.shape[0] == 2 + + result = np.zeros(uv_points.shape[1]) + for i, uv in enumerate(uv_points.T): + iuv = np.floor(uv / self.uv_step) + iu = max(0, min(self.shape[0] - 2, int(iuv[0]))) + iv = max(0, min(self.shape[1] - 2, int(iuv[1]))) + iuv = np.array([iu, iv]) + + uv_loc = uv / self.uv_step - iuv + u_loc = np.array([1 - uv_loc[0], uv_loc[0]]) + v_loc = np.array([1 - uv_loc[1], uv_loc[1]]) + Z_mat = self.grid[iv: (iv + 2), iu: (iu + 2)] + result[i] = self.z_surface.z_scale*(v_loc.dot(Z_mat).dot(u_loc)) + self.z_surface.z_shift + return result diff --git a/src/bspline_plot.py b/src/bspline_plot.py index 74a4a65..6c843d8 100644 --- a/src/bspline_plot.py +++ b/src/bspline_plot.py @@ -60,9 +60,9 @@ def plot_surface_3d(surface, fig_ax, n_points=(100, 100), **kwargs): v_coord = np.linspace(v_basis.domain[0], v_basis.domain[1], n_points[1]) U, V = np.meshgrid(u_coord, v_coord) - points = np.vstack( [U.ravel(), V.ravel()] ) + points = np.stack( [U.ravel(), V.ravel()], axis = 1 ) - xyz = np.array([surface.eval(point[0], point[1]) for point in points.T ]) + xyz = surface.eval_array(points) X, Y, Z = xyz.T X = X.reshape(U.shape) diff --git a/tests/test_bspline.py b/tests/test_bspline.py index 0cf26ec..b20dea6 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -64,7 +64,6 @@ def test_linear_poles(self): poles = eq_basis.make_linear_poles() t_vec = np.linspace(0.0, 1.0, 21) - x_vec = [] for t in t_vec: b_vals = np.array([ eq_basis.eval(i, t) for i in range(eq_basis.size) ]) x = np.dot(b_vals, poles) @@ -96,6 +95,13 @@ def test_evaluate(self): def make_function_grid(fn, nu, nv): + """ + Make a grid of points on a graph of the function. + :param fn: fn( [x, y] ) -> z + :param nu: n-points in u-direction + :param nv: n-points in v-direction + :return: array of points: nu x nv x 3 + """ X_grid = np.linspace(0, 1.0, nu) Y_grid = np.linspace(0, 1.0, nv) Y, X = np.meshgrid(Y_grid, X_grid) @@ -157,7 +163,7 @@ def test_evaluate(self): class TestZ_Surface: - + # TODO: Compute max norm of the difference of two surfaces and assert that it is cose to zero. def plot_function_uv(self): @@ -185,5 +191,65 @@ def function(x): plt.show() def test_eval_uv(self): - self.plot_function_uv() - pass \ No newline at end of file + #self.plot_function_uv() + pass + + + +class TestPointGrid: + + @staticmethod + def function(x): + return math.sin(x[0]) * math.cos(x[1]) + + + def make_point_grid(self): + nu, nv = 5,6 + grid = make_function_grid(TestPointGrid.function, 5, 6).reshape(nu*nv, 3) + surf = bs.GridSurface() + surf.init_from_seq(grid.T) + return surf + + def check_surface(self, surf, xy_mat, xy_shift, z_mat): + """ + TODO: Make this a general function - evaluate a surface on a grid, use it also in other tests + to compare evaluation on the grid to the original function. Can be done after we have approximations. + """ + + nu, nv = 30, 40 + # surface on unit square + U = np.linspace(0.0, 1.0, nu) + V = np.linspace(0.0, 1.0, nv) + U_grid, V_grid = np.meshgrid(U,V) + + UV = np.vstack([U_grid.ravel(), V_grid.ravel()]) + XY = xy_mat.dot(UV).T + xy_shift + Z_grid = surf.z_eval_xy_array(XY.T) + eps = 0.0 + hx = 1.0 / surf.shape[0] + hy = 1.0 / surf.shape[1] + tol = 0.5* ( hx*hx + 2*hx*hy + hy*hy) + for xy, z_approx in zip(UV.T, Z_grid): + z_func = z_mat[0]*self.function(xy) + z_mat[1] + eps = max(eps, math.fabs( z_approx - z_func)) + assert math.fabs( z_approx - z_func) < tol + print("Max norm: ", eps, "Tol: ", tol) + + def test_grid_surface(self): + xy_mat = np.array([ [1.0, 0.0], [0.0, 1.0] ]) + xy_shift = np.array([0.0, 0.0 ]) + z_shift = np.array([1.0, 0.0]) + surface = self.make_point_grid() + + self.check_surface(surface, xy_mat, xy_shift, z_shift) + + # transformed surface + xy_mat = np.array([ [3.0, -3.0], [2.0, 2.0] ]) / math.sqrt(2) + xy_shift = np.array([[-2.0, 5.0 ]]) + z_shift = np.array([1.0, 1.3]) + + surface = self.make_point_grid() + surface.z_surface.transform(np.concatenate((xy_mat, xy_shift.T), axis=1), z_shift) + self.check_surface(surface, xy_mat, xy_shift, z_shift) + + From e53ee3ad0dc65911379a55616e34623a7f9c960a Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Sun, 17 Sep 2017 13:35:25 +0200 Subject: [PATCH 17/36] Import surface approximation from bapprox - reuse functions from SplineBasis - add optimized versions of basis evaluation - simplify approximation code - graphical tests --- src/bspline.py | 185 ++++++++++++++++---- src/bspline_approx.py | 374 ++++++++++++++++++++++++++++++++++++++++ tests/test_bs_approx.py | 46 +++++ tests/test_bspline.py | 124 +++++++++---- 4 files changed, 666 insertions(+), 63 deletions(-) create mode 100644 src/bspline_approx.py create mode 100644 tests/test_bs_approx.py diff --git a/src/bspline.py b/src/bspline.py index f075bfb..b71079c 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -138,8 +138,12 @@ def __init__(self, degree, knots): self.knots_idx_range = [self.degree, len(self.knots) - self.degree - 1] self.domain = self.knots[self.knots_idx_range] self.domain_size = self.domain[1] - self.domain[0] + self.n_intervals = self.size - self.degree # Number of basis functions. + if self.degree == 2: + self.eval_base_vector = self._eval_base_vector_deg_2 + self.eval_diff_base_vector = self._eval_diff_base_vector_deg_2 def pack_knots(self): last, mult = self.knots[0], 0 @@ -236,6 +240,7 @@ def fn_supp(self, i_base): """ return (self.knots[i_base], self.knots[i_base + self.degree + 1]) + def eval(self, i_base, t): """ :param i_base: Index of base function to evaluate. @@ -247,6 +252,28 @@ def eval(self, i_base, t): return 1.0 return self._basis(self.degree, i_base, t) + + def eval_diff(self, i_base, t): + assert 0 <= i_base < self.size + deg = self.degree + i = i_base + if i_base == self.size - 2 and t == self.domain[1]: + return - deg / (self.knots[i + deg + 1] - self.knots[i + 1]) + + if i_base == self.size - 1 and t == self.domain[1]: + return deg / (self.knots[i + deg] - self.knots[i]) + + diff = 0.0 + if i > 0: + B = self._basis(deg-1, i, t) + diff += deg * B / (self.knots[i + deg] - self.knots[i]) + if i < self.size - 1: + B = self._basis(deg-1, i + 1, t) + diff -= deg * B / (self.knots[i + deg + 1] - self.knots[i + 1]) + + return diff + + def make_linear_poles(self): """ Return poles of basis functions to get a f(x) = x. @@ -259,6 +286,77 @@ def make_linear_poles(self): return poles + def eval_base_vector(self, i_base, t): + values = [] + for ib in range(i_base, i_base + self.degree + 1): + values.append( self.eval(ib, t)) + return values + + + def eval_diff_base_vector(self, i_base, t): + values = [] + for ib in range(i_base, i_base + self.degree + 1): + values.append( self.eval_diff(ib, t)) + return values + + + def _eval_base_vector_deg_2(self, i_base, t): + """ + This function compute normalized blending function aka base function of B-Spline curve or surface. + :param knot_vec: + :param t_param: + :param order: (0: function value, 1: derivative function value) + :param sparse: + :return: + """ + + basis_values = np.zeros(3) + + tk1, tk2, tk3, tk4 = self.knots[i_base + 1 : i_base + 5] + + d31 = tk3 - tk1 + d32 = tk3 - tk2 + d42 = tk4 - tk2 + + dt1 = t - tk1 + dt2 = t - tk2 + d3t = tk3 - t + d4t = tk4 - t + + d31_d32 = d31 * d32 + d42_d32 = d42 * d32 + + basis_values[0] = (d3t * d3t) / d31_d32 + basis_values[1] = ((dt1 * d3t) / d31_d32) + ((dt2 * d4t) / d42_d32) + basis_values[2] = (dt2 * dt2) / d42_d32 + + return basis_values + + + def _eval_diff_base_vector_deg_2(self, i_base, t): + + basis_values = np.zeros(3) + + tk1, tk2, tk3, tk4 = self.knots[i_base + 1: i_base + 5] + + d31 = tk3 - tk1 + d32 = tk3 - tk2 + d42 = tk4 - tk2 + + dt1 = t - tk1 + dt2 = t - tk2 + d3t = tk3 - t + d4t = tk4 - t + + d31_d32 = d31 * d32 + d42_d32 = d42 * d32 + + basis_values[0] = -2 * d3t / d31_d32 + basis_values[1] = (d3t - dt1) / d31_d32 + (d4t - dt2) / d42_d32 + basis_values[2] = 2 * dt2 / d42_d32 + + return basis_values + class Curve: @@ -408,8 +506,8 @@ def __init__(self, xy_quad, z_surface): for the bilinear UV -> XY mapping. :param xy_quad: np array N x 2 Four or three points, determining bilinear or linear mapping, respectively. - Four points giving XY coordinates for the uv corners: (0,0), (0,1), (1,0), (1,1) - Three points giving XY coordinates for the uv corners: (0,0), (0,1), (1,0) + Four points giving XY coordinates for the uv corners: (0,1), (0,0), (1,0), (1,1) + Three points giving XY coordinates for the uv corners: (0,1), (0,0), (1,0) :param z_surface: !D Surface object. """ assert z_surface.dim == 1 @@ -423,9 +521,9 @@ def __init__(self, xy_quad, z_surface): if len(xy_quad) == 3: # linear case - self.xy_shift = xy_quad[0] - v_vec = xy_quad[1] - xy_quad[0] - u_vec = xy_quad[2] - xy_quad[0] + self.xy_shift = xy_quad[1] + v_vec = xy_quad[0] - xy_quad[1] + u_vec = xy_quad[2] - xy_quad[1] self.mat_uv_to_xy = np.column_stack((u_vec, v_vec)) self.mat_xy_to_uv = la.inv(self.mat_uv_to_xy) @@ -486,14 +584,15 @@ def uv_to_xy(self, uv_points): :return: numpy array N x [x,y] """ def _linear_uv_to_xy(self, uv_points): - assert uv_points.shape[0] == 2, "Size: {}".format(uv_points.shape) - return (self.mat_uv_to_xy.dot(uv_points).T + self.xy_shift).T + assert uv_points.shape[1] == 2, "Size: {}".format(uv_points.shape) + return ( np.dot(uv_points, self.mat_uv_to_xy.T) + self.xy_shift) def _bilinear_uv_to_xy(self, uv_points): + assert uv_points.shape[1] == 2, "Size: {}".format(uv_points.shape) xy_list = [] for u,v in uv_points: - weights = np.array([ (1-u)*(1-v), (1-u)*v, u*(1-v), u*v ]) + weights = np.array([ (1-u)*v, (1-u)*(1-v), u*(1-v), u*v ]) xy_list.append( self.quad.T.dot(weights) ) return np.array(xy_list) @@ -506,11 +605,12 @@ def xy_to_uv(self, xy_points): """ def _linear_xy_to_uv(self, xy_points): # assert xy_points.shape[0] == 2 - assert xy_points.shape[0] == 2 - return self.mat_xy_to_uv.dot((xy_points.T - self.xy_shift).T) + assert xy_points.shape[1] == 2 + return np.dot((xy_points - self.xy_shift), self.mat_xy_to_uv.T) def _bilinear_xy_to_uv(self, xy_points): + assert xy_points.shape[1] == 2 assert False, "Not implemented yet." @@ -535,12 +635,12 @@ def eval_xy_array(self, xy_points): def z_eval_array(self, uv_points): z_points = self.z_surface.eval_array(uv_points) - return z_points + return z_points.reshape(-1) - def z_eval_xy_array(self, uv_points): + def z_eval_xy_array(self, xy_points): uv_points = self.xy_to_uv(xy_points) z_points = self.z_surface.eval_array(uv_points) - return z_points + return z_points.reshape(-1) class InvalidGridExc(Exception): pass @@ -632,24 +732,29 @@ def init_from_seq(self, point_seq): if pred_y > 0 and not la.norm(point_seq_xy[:, i] - point_seq_xy[:, pred_y] - v_step) < 2*self.step_tolerance: raise InvalidGridExc("Irregular grid in Y direction, point %d"%i) - quad = np.stack([vtx_00, vtx_01, vtx_10], axis = 0) - u_basis = SplineBasis.make_equidistant(1, nu-1) - v_basis = SplineBasis.make_equidistant(1, nv-1) + #self.uv_to_xy(np.array([[0, 1], [0, 0], [1, 0], [1, 1]]).T) + self.quad = np.stack([vtx_01, vtx_00, vtx_10, vtx_11], axis = 0) + # Envelope quad - polygon, oriented counter clockwise. - self.grid = point_seq[2, :].reshape(nv, nu) - poles_z = np.transpose( point_seq[2, :].reshape(nv, nu, 1), axes = [1, 0, 2] ) - self.z_surface = Z_Surface(quad, Surface((u_basis, v_basis), poles_z) ) + self.grid_z = point_seq[2, :].reshape(nv, nu) + # grid of original Z values, format as matrix self.shape = (nu, nv) + # Grid shape. + self.uv_step = (1.0 / float(nu - 1), 1.0 / float(nv - 1)) + # Grid step in u, v direction respectively. + + self.points_xyz = point_seq.T + # Original sequance of XYZ points + + u_basis = SplineBasis.make_equidistant(1, nu-1) + v_basis = SplineBasis.make_equidistant(1, nv-1) + poles_z = np.transpose( point_seq[2, :].reshape(nv, nu, 1), axes = [1, 0, 2] ) + self.z_surface = Z_Surface(self.quad[0:3], Surface((u_basis, v_basis), poles_z) ) + # related bilinear Z-surface, all evaluations just call this object. - def get_xy_envelope(self): - """ - Returns the envelope of the surface domain in XY plane as a quadrilateral polygon. - :return: Polygon as a array of the XY points in conunter clockwise direction. - """ - return self.uv_to_xy(np.array([[0,0], [1,0], [1,1], [0,1]]).T) @@ -679,7 +784,7 @@ def z_eval_xy_array(self, xy_points): :param points: np array N x 2 of XY cooridinates :return: array of Z values """ - assert xy_points.shape[0] == 2, "Size: {}".format(xy_points.shape) + assert xy_points.shape[1] == 2, "Size: {}".format(xy_points.shape) uv_points = self.z_surface.xy_to_uv(xy_points) return self.z_eval_array(uv_points) @@ -691,10 +796,10 @@ def z_eval_array(self, uv_points): :return: array of Z values """ - assert uv_points.shape[0] == 2 + assert uv_points.shape[1] == 2 - result = np.zeros(uv_points.shape[1]) - for i, uv in enumerate(uv_points.T): + result = np.zeros(uv_points.shape[0]) + for i, uv in enumerate(uv_points): iuv = np.floor(uv / self.uv_step) iu = max(0, min(self.shape[0] - 2, int(iuv[0]))) iv = max(0, min(self.shape[1] - 2, int(iuv[1]))) @@ -703,7 +808,7 @@ def z_eval_array(self, uv_points): uv_loc = uv / self.uv_step - iuv u_loc = np.array([1 - uv_loc[0], uv_loc[0]]) v_loc = np.array([1 - uv_loc[1], uv_loc[1]]) - Z_mat = self.grid[iv: (iv + 2), iu: (iu + 2)] + Z_mat = self.grid_z[iv: (iv + 2), iu: (iu + 2)] result[i] = self.z_surface.z_scale*(v_loc.dot(Z_mat).dot(u_loc)) + self.z_surface.z_shift return result @@ -711,6 +816,26 @@ def z_eval_array(self, uv_points): +def make_function_grid(fn, nu, nv): + """ + Make a grid of points on a graph of the function. + :param fn: fn( [x, y] ) -> z + :param nu: n-points in u-direction + :param nv: n-points in v-direction + :return: array of points: nu x nv x 3 + """ + X_grid = np.linspace(0, 1.0, nu) + Y_grid = np.linspace(0, 1.0, nv) + Y, X = np.meshgrid(Y_grid, X_grid) + + points_uv = np.stack([X.ravel(), Y.ravel()], 1) + Z = np.apply_along_axis(fn, 1, points_uv) + points = np.stack([X.ravel(), Y.ravel(), Z], 1) + + return points.reshape( (nu, nv, 3) ) + + + diff --git a/src/bspline_approx.py b/src/bspline_approx.py new file mode 100644 index 0000000..bafde5a --- /dev/null +++ b/src/bspline_approx.py @@ -0,0 +1,374 @@ +""" +Collection of functions to produce Bapline curves and surfaces as approximation of various analytical curves and surfaces. +""" + +import bspline as bs +import numpy as np +import math +import time +#import numpy.linalg +import scipy.sparse +import scipy.sparse.linalg + + +""" +Approximation methods for B/splines of degree 2. + +""" +def plane_surface(vtxs): + """ + Returns B-spline surface of a plane given by 3 points. + We retun also list of UV coordinates of the given points. + :param vtxs: List of tuples (X,Y,Z) + :return: ( Surface, vtxs_uv ) + """ + assert len(vtxs) == 3, "n vtx: {}".format(len(vtxs)) + vtxs.append( (0,0,0) ) + vtxs = np.array(vtxs) + vv = vtxs[1] + vtxs[2] - vtxs[0] + vtx4 = [ vtxs[0], vtxs[1], vv, vtxs[2]] + (surf, vtxs_uv) = bilinear_surface(vtx4) + return (surf, [ vtxs_uv[0], vtxs_uv[1], vtxs_uv[3] ]) + + + +def bilinear_surface(vtxs): + """ + Returns B-spline surface of a bilinear surface given by 4 corner points. + We retun also list of UV coordinates of the given points. + :param vtxs: List of tuples (X,Y,Z) + :return: ( Surface, vtxs_uv ) + """ + assert len(vtxs) == 4, "n vtx: {}".format(len(vtxs)) + vtxs = np.array(vtxs) + def mid(*idx): + return np.mean( vtxs[list(idx)], axis=0) + + # v - direction v0 -> v2 + # u - direction v0 -> v1 + poles = [ [vtxs[0], mid(0, 3), vtxs[3]], + [mid(0,1), mid(0,1,2,3), mid(2,3)], + [vtxs[1], mid(1,2), vtxs[2]] + ] + knots = [(0.0, 3), (1.0, 3)] + basis = bs.SplineBasis(2, knots) + surface = bs.Surface((basis, basis), poles) + vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] + return (surface, vtxs_uv) + + + + +def _line_data(vtxs, overhang=0.0): + ''' + :param vtxs: List of tuples (X,Y) or (X,Y,Z) + :return: + ''' + assert len(vtxs) == 2 + vtxs = np.array(vtxs) + mid = np.mean(vtxs, axis=0) + poles = [ vtxs[0], mid, vtxs[1] ] + knots = [(0.0+overhang, 3), (1.0-overhang, 3)] + basis = bs.SplineBasis(2, knots) + return (basis, poles) + + + +def line_2d(vtxs): + """ + Return B-spline approximation of line from two 2d points + :param vtxs: [ (X0, Y0), (X1, Y1) ] + :return: Curve2D + """ + return bs.Curve( *_line_data(vtxs)) + + + +def line_3d(vtxs): + """ + Return B-spline approximation of line from two 3d points + :param vtxs: [ (X0, Y0, Z0), (X1, Y1, Z0) ] + :return: Curve2D + """ + return bs.Curve(*_line_data(vtxs)) + + + +def from_grid(grid_surface, nuv, **kwargs): + """ + Make a Z_Surface of degree 2 as an approximation of the GridSurface. + :param grid_surface: grid surface to approximate + :param (nu, nv) Prescribed number of poles in u and v directions. + :return: Z_surface object. + """ + approx = _SurfaceApprox(grid_surface, nuv, **kwargs) + return approx.get_approximation() + + + + + + + + +class _SurfaceApprox: + + + + def __init__(self, grid_surface, nuv, **kwargs): + self.grid_surf = grid_surface + self.u_basis = bs.SplineBasis.make_equidistant(2, nuv[0]) + self.v_basis = bs.SplineBasis.make_equidistant(2, nuv[1]) + self.regularization_weight = kwargs.get('reg_weight', 1.0) + + def get_approximation(self): + if not hasattr(self, 'z_surf'): + self.z_surf = self.approx_chol() + return self.z_surf + + + def approx_chol(self): + """ + This function tries to approximate terrain data with B-Spline surface patches + using Cholesky decomposition + :param terrain_data: matrix of 3D terrain data + :param quad: points defining quadrangle area (array) + :param u_knots: array of u knots + :param v_knots: array of v knots + :param sparse: if sparse matrix is used + :param filter_thresh: threshold of filter + :return: B-Spline patch + """ + + print('Transforming points to parametric space ...') + start_time = time.time() + points = self.grid_surf.points_xyz + points_uv = self.grid_surf.xy_to_uv( points[:, 0:2] ) + + + # remove points far from unit square + eps = 1.0e-15 + cut_min = np.array( [ -eps, -eps ]) + cut_max = np.array( [ 1+eps, 1+eps ]) + + in_idx = np.all(np.logical_and(cut_min < points_uv, points_uv <= cut_max), axis=1) + points_uv = points_uv[in_idx] + points_z = points[in_idx, 2][:,None] + + # snap to unit square + points_uv = np.maximum(points_uv, np.array([0.0, 0.0])) + points_uv = np.minimum(points_uv, np.array([1.0, 1.0])) + + + self.grid_uvz = np.concatenate((points_uv, points_z), axis=1) + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) + + # Own computation of approximation + print('Creating B matrix ...') + start_time = time.time() + b_mat, interval = self.build_ls_matrix() + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) + + print('Creating A matrix ...') + start_time = time.time() + a_mat = self.build_sparse_reg_matrix() + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) + + print('Computing B^T B matrix ...') + start_time = time.time() + bb_mat = b_mat.transpose() * b_mat + + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) + + + bb_norm = scipy.sparse.linalg.svds(bb_mat, k=1, ncv=10, tol=1e-4, which='LM', v0=None, + maxiter=300, return_singular_vectors=False) + a_norm = scipy.sparse.linalg.svds(a_mat, k=1, ncv=10, tol=1e-4, which='LM', v0=None, + maxiter=300, return_singular_vectors=False) + c_mat = bb_mat + self.regularization_weight * (bb_norm[0] / a_norm[0]) * a_mat + + g_vec = self.grid_uvz[:, 2] + b_vec = b_mat.transpose() * g_vec + + + print('Computing Z coordinates ...') + start_time = time.time() + z_vec = scipy.sparse.linalg.spsolve(c_mat, b_vec) + print(type(z_vec)) + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) + + + # print('Computing differences ...') + # start_time = time.time() + # diff = (numpy.matrix(b_mat * z_vec).transpose() - g_vec).tolist() + # diff = [item[0] for item in diff] + # end_time = time.time() + # print('Computed in {0} seconds.'.format(end_time - start_time)) + + # Construct Z-Surface + poles_z = z_vec.reshape(self.u_basis.size, self.v_basis.size, 1) + surface_z = bs.Surface((self.u_basis, self.v_basis), poles_z) + z_surf = bs.Z_Surface(self.grid_surf.quad, surface_z) + + return z_surf + + + + def build_ls_matrix(self): + """ + Construction of the matrix (B) of the system of linear algebraic + equations for control points of the 2th order B-spline surface + :param u_knots: + :param v_knots: + :param terrain: + :param sparse: + :return: + """ + u_n_basf = self.u_basis.size + v_n_basf = self.v_basis.size + n_points = self.grid_uvz.shape[0] + + n_uv_loc_nz = (self.u_basis.degree + 1) * (self.v_basis.degree + 1) + row = np.zeros(n_points * n_uv_loc_nz) + col = np.zeros(n_points * n_uv_loc_nz) + data = np.zeros(n_points * n_uv_loc_nz) + + nnz_b = 0 + + interval = np.empty((n_points, 2)) + + for idx in range(0, n_points): + u, v = self.grid_uvz[idx, 0:2] + iu = self.u_basis.find_knot_interval(u) + iv = self.u_basis.find_knot_interval(v) + u_base_vec = self.u_basis.eval_base_vector(iu, u) + v_base_vec = self.u_basis.eval_base_vector(iv, v) + # Hard-coded Kronecker product (problem based) + for n in range(0, 3): + data[nnz_b + 3 * n:nnz_b + 3 * (n + 1)] = v_base_vec[n] * u_base_vec + for m in range(0, 3): + col[nnz_b + (3 * n) + m] = (iv + n) * u_n_basf + iu + m + row[nnz_b:nnz_b + 9] = idx + nnz_b += 9 + + interval[idx][0] = iu + interval[idx][1] = iv + + mat_b = scipy.sparse.csr_matrix((data, (row, col)), shape=(n_points, u_n_basf * v_n_basf)) + + return mat_b, interval + + def _basis_in_q_points(self, basis): + n_int = basis.n_intervals + nq_points = len(self._q_points) + point_val = np.zeros((3, n_int * nq_points)) + d_point_val = np.zeros((3, n_int * nq_points)) + point_idx = np.zeros((n_int * nq_points, 1)) + q_point = np.zeros((n_int * nq_points, 1)) + + n = 0 + for i in range(n_int): + us = basis.knots[i + 2] + uil = basis.knots[i + 3] - basis.knots[i + 2] + for j in range(nq_points): + up = us + uil * self._q_points[j] + q_point[n] = up + idx = basis.find_knot_interval(up) + u_base_vec = basis.eval_base_vector(idx, up) + u_base_vec_diff = basis.eval_diff_base_vector(idx, up) + point_val[:, n] = u_base_vec + d_point_val[:, n] = u_base_vec_diff + point_idx[n] = idx + n += 1 + return point_val, d_point_val, point_idx, q_point + + def build_sparse_reg_matrix(self): + """ + Construction of the regularization matrix (A) to decrease variation of the terrain + B z = b ---> (B^T B + A)z = B^T b + :param u_knots: vector of v-knots + :param v_knots: vector of u-knots + :param quad: points defining quadrangle area (array) + :return: matrix + + - + """ + + #a = quad[:, 3] - quad[:, 2] + #b = quad[:, 0] - quad[:, 1] + #c = quad[:, 1] - quad[:, 2] + #d = quad[:, 0] - quad[:, 3] + + u_n_basf = self.u_basis.size + v_n_basf = self.v_basis.size + u_n_inter = self.u_basis.n_intervals + v_n_inter = self.v_basis.n_intervals + n_uv_loc_nz = (self.u_basis.degree + 1) * (self.v_basis.degree + 1) + + # TODO: use Gauss quadrature from scipy + # in fact for general degrees we should use different quadrature for u and different for v + self._q_points = [0, (0.5 - 1 / math.sqrt(20)), (0.5 + 1 / math.sqrt(20)), 1] + self._weights = [1.0 / 6, 5.0 / 6, 5.0 / 6, 1.0 / 6] + nq_points = len(self._q_points) + + # TODO: rename: u_vals, u_diffs, u_idxs, u_points + u_point_val, ud_point_val, u_point_idx, q_u_point = self._basis_in_q_points(self.u_basis) + v_point_val, vd_point_val, v_point_idx, q_v_point = self._basis_in_q_points(self.v_basis) + + # Matrix construction + # TODO: Assembly local dense blocks 9*9 and then put these nonzeroes into sparse matirx + # TODO: use numpy opperations to make assembly of local blocks readable, eliminate loops + colv = np.zeros(n_uv_loc_nz) + # TODO:rename data and data2 to something meqningful + data = np.zeros(n_uv_loc_nz) + data2 = np.zeros(n_uv_loc_nz) + row_m = np.zeros((v_n_inter * u_n_inter * nq_points * nq_points * n_uv_loc_nz * n_uv_loc_nz)) + col_m = np.zeros((v_n_inter * u_n_inter * nq_points * nq_points * n_uv_loc_nz * n_uv_loc_nz)) + data_m = np.zeros((v_n_inter * u_n_inter * nq_points * nq_points * n_uv_loc_nz * n_uv_loc_nz)) + nnz_a = 0 + + + for i in range(v_n_inter): + for k in range(nq_points): + v_point = v_point_val[:, i * nq_points + k] + vd_point = vd_point_val[:, i * nq_points + k] + j_idx = v_point_idx[i * nq_points + k] + for l in range(u_n_inter): + for m in range(nq_points): + u_point = u_point_val[:, l * nq_points + m] + ud_point = ud_point_val[:, l * nq_points + m] + i_idx = u_point_idx[l * nq_points + m] + for n in range(0, 3): + # Hard-coded Kronecker product: vd = numpy.kron(vd_point, u_point) + data[3 * n:3 * (n + 1)] = vd_point[n] * u_point + # Hard-coded Kronecker product: ud = numpy.kron(v_point, ud_point) + data2[3 * n:3 * (n + 1)] = v_point[n] * ud_point + # column indices for data & data2 + for p in range(0, 3): + colv[3 * n + p] = (j_idx + n) * u_n_basf + i_idx + p + + # Hard-coded Outer product: + # Jacobian * weights[m] * weights[k] * (numpy.outer(ud, ud) + numpy.outer(vd, vd)) + #u_q = q_u_point[l * nq_points + m, 0] + #v_q = q_v_point[i * nq_points + k, 0] + + # jacobian for UV coordinates should be used + jac = 1.0 / u_n_inter / v_n_inter + coef = self._weights[m] * self._weights[k] * jac + for n in range(0, 9): + row_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv + col_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv[n] + data_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = coef * (data[n] * data + data2[n] * data2) + nnz_a += n_uv_loc_nz * n_uv_loc_nz + + mat_a = scipy.sparse.coo_matrix((data_m, (row_m, col_m)), + shape=(u_n_basf * v_n_basf, u_n_basf * v_n_basf)).tocsr() + + return mat_a + + diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py new file mode 100644 index 0000000..2139527 --- /dev/null +++ b/tests/test_bs_approx.py @@ -0,0 +1,46 @@ +import pytest +import numpy as np +import bspline as bs +import bspline_approx as bs_approx +import math +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +import bspline_plot as bs_plot + +def function_sin_cos(x): + return math.sin(x[0] * 4) * math.cos(x[1] * 4) + + +class TestSurfaceApprox: + + def test_approx_grid(self): + points = bs.make_function_grid(function_sin_cos, 20, 30) + gs = bs.GridSurface() + gs.init_from_seq(points.reshape(-1, 3).T) + z_surf = bs_approx.from_grid(gs, (3,4) ) + + fig = plt.figure() + ax = fig.gca(projection='3d') + + bs_plot.plot_surface_3d(z_surf, ax) + + plt.show() + + def test_approx_transformed_grid(self): + points = bs.make_function_grid(function_sin_cos, 20, 30) + mat = np.array( [ [2.0, 1.0, 0.0 ], [1.0, 2.0, 0.0 ], [0.0, 0.0, 0.5 ]]) + points = np.dot(points, mat.T) + np.array([10, 20, -5.0]) + gs = bs.GridSurface() + gs.init_from_seq(points.reshape(-1, 3).T) + z_surf = bs_approx.from_grid(gs, (3,4) ) + + fig = plt.figure() + ax = fig.gca(projection='3d') + + bs_plot.plot_surface_3d(z_surf, ax) + + plt.show() + + # todo: transform grid points to uV in Gridsurface + # todo: keep quad in z_surf (OK) + # check that conversion to full keeps the transformation \ No newline at end of file diff --git a/tests/test_bspline.py b/tests/test_bspline.py index b20dea6..226b0ef 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -20,12 +20,9 @@ def test_find_knot_interval(self): assert eq_basis.find_knot_interval(1.0) == 99 def plot_basis(self, degree): - eq_basis = bs.SplineBasis.make_equidistant(degree, 4) - n_points = 401 - dx = (eq_basis.domain[1] - eq_basis.domain[0]) / (n_points -1) - x_coord = [eq_basis.domain[0] + dx * i for i in range(n_points)] + x_coord = np.linspace(eq_basis.domain[0], eq_basis.domain[1], n_points) for i_base in range(eq_basis.size): y_coord = [ eq_basis.eval(i_base, x) for x in x_coord ] @@ -70,6 +67,85 @@ def test_linear_poles(self): assert np.abs( x - t ) < 1e-15 + + def check_eval_vec(self, basis, i, t): + vec = basis.eval_base_vector(i, t) + for j in range(basis.degree + 1): + assert vec[j] == basis.eval(i + j, t) + + def plot_basis_vec(self, basis): + n_points = 401 + x_coord = np.linspace(basis.domain[0], basis.domain[1], n_points) + + y_coords = np.zeros( (basis.size, x_coord.shape[0]) ) + for i, x in enumerate(x_coord): + idx = basis.find_knot_interval(x) + y_coords[idx : idx + basis.degree + 1, i] = basis.eval_base_vector(idx, x) + + for i_base in range(basis.size): + plt.plot(x_coord, y_coords[i_base, :]) + + plt.show() + + + def test_eval_base_vec(self): + basis = bs.SplineBasis.make_equidistant(2, 4) + # self.plot_basis_vec(basis) + self.check_eval_vec(basis, 0, 0.1) + self.check_eval_vec(basis, 1, 0.3) + self.check_eval_vec(basis, 2, 0.6) + self.check_eval_vec(basis, 3, 0.8) + self.check_eval_vec(basis, 3, 1.0) + + + basis = bs.SplineBasis.make_equidistant(3, 4) + # self.plot_basis_vec(basis) + self.check_eval_vec(basis, 0, 0.1) + self.check_eval_vec(basis, 1, 0.3) + self.check_eval_vec(basis, 2, 0.6) + self.check_eval_vec(basis, 3, 0.8) + self.check_eval_vec(basis, 3, 1.0) + + + + def check_diff_vec(self, basis, i, t): + vec = basis.eval_diff_base_vector(i, t) + for j in range(basis.degree + 1): + assert np.abs(vec[j] - basis.eval_diff(i + j, t)) < 1e-15 + + def plot_basis_diff(self, basis): + n_points = 401 + x_coord = np.linspace(basis.domain[0], basis.domain[1], n_points) + + y_coords = np.zeros( (basis.size, x_coord.shape[0]) ) + for i, x in enumerate(x_coord): + idx = basis.find_knot_interval(x) + y_coords[idx : idx + basis.degree + 1, i] = basis.eval_diff_base_vector(idx, x) + + for i_base in range(basis.size): + plt.plot(x_coord, y_coords[i_base, :]) + + plt.show() + + + def test_eval_diff_base_vec(self): + basis = bs.SplineBasis.make_equidistant(2, 4) + # self.plot_basis_diff(basis) + self.check_diff_vec(basis, 0, 0.1) + self.check_diff_vec(basis, 1, 0.3) + self.check_diff_vec(basis, 2, 0.6) + self.check_diff_vec(basis, 3, 0.8) + self.check_diff_vec(basis, 3, 1.0) + + basis = bs.SplineBasis.make_equidistant(3, 4) + # self.plot_basis_diff(basis) + self.check_diff_vec(basis, 0, 0.1) + self.check_diff_vec(basis, 1, 0.3) + self.check_diff_vec(basis, 2, 0.6) + self.check_diff_vec(basis, 3, 0.8) + self.check_diff_vec(basis, 3, 1.0) + + class TestCurve: def plot_4p(self): @@ -83,7 +159,7 @@ def plot_4p(self): plt.show() def test_evaluate(self): - #self.plot_4p() + # self.plot_4p() pass # TODO: test rational curves, e.g. circle @@ -94,26 +170,6 @@ def test_evaluate(self): -def make_function_grid(fn, nu, nv): - """ - Make a grid of points on a graph of the function. - :param fn: fn( [x, y] ) -> z - :param nu: n-points in u-direction - :param nv: n-points in v-direction - :return: array of points: nu x nv x 3 - """ - X_grid = np.linspace(0, 1.0, nu) - Y_grid = np.linspace(0, 1.0, nv) - Y, X = np.meshgrid(Y_grid, X_grid) - - points_uv = np.stack([X.ravel(), Y.ravel()], 1) - Z = np.apply_along_axis(fn, 1, points_uv) - points = np.stack([X.ravel(), Y.ravel(), Z], 1) - - return points.reshape( (nu, nv, 3) ) - - - class TestSurface: @@ -140,7 +196,7 @@ def function(x): fig = plt.figure() ax = fig.gca(projection='3d') - poles = make_function_grid(function, 4, 5) + poles = bs.make_function_grid(function, 4, 5) u_basis = bs.SplineBasis.make_equidistant(2, 2) v_basis = bs.SplineBasis.make_equidistant(2, 3) surface_func = bs.Surface( (u_basis, v_basis), poles) @@ -153,8 +209,8 @@ def test_evaluate(self): from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt - #self.plot_extrude() - #self.plot_function() + # self.plot_extrude() + # self.plot_function() # TODO: test rational surfaces, e.g. sphere @@ -174,7 +230,7 @@ def function(x): fig = plt.figure() ax = fig.gca(projection='3d') - poles = make_function_grid(function, 4, 5) + poles = bs.make_function_grid(function, 4, 5) u_basis = bs.SplineBasis.make_equidistant(2, 2) v_basis = bs.SplineBasis.make_equidistant(2, 3) surface_func = bs.Surface( (u_basis, v_basis), poles[:,:, [2] ]) @@ -205,7 +261,7 @@ def function(x): def make_point_grid(self): nu, nv = 5,6 - grid = make_function_grid(TestPointGrid.function, 5, 6).reshape(nu*nv, 3) + grid = bs.make_function_grid(TestPointGrid.function, 5, 6).reshape(nu*nv, 3) surf = bs.GridSurface() surf.init_from_seq(grid.T) return surf @@ -224,13 +280,15 @@ def check_surface(self, surf, xy_mat, xy_shift, z_mat): UV = np.vstack([U_grid.ravel(), V_grid.ravel()]) XY = xy_mat.dot(UV).T + xy_shift - Z_grid = surf.z_eval_xy_array(XY.T) + Z_grid = surf.z_eval_xy_array(XY) eps = 0.0 hx = 1.0 / surf.shape[0] hy = 1.0 / surf.shape[1] tol = 0.5* ( hx*hx + 2*hx*hy + hy*hy) - for xy, z_approx in zip(UV.T, Z_grid): - z_func = z_mat[0]*self.function(xy) + z_mat[1] + + uvz = np.concatenate( (UV.T, Z_grid[:, None]), axis = 1) + for u, v, z_approx in uvz: + z_func = z_mat[0]*self.function([u,v]) + z_mat[1] eps = max(eps, math.fabs( z_approx - z_func)) assert math.fabs( z_approx - z_func) < tol print("Max norm: ", eps, "Tol: ", tol) From f8759049950264d9321192349ab921d4def4eeb8 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Sun, 17 Sep 2017 15:35:31 +0200 Subject: [PATCH 18/36] Fix find_knots_interval --- src/bspline.py | 32 +++----------------------------- tests/test_bspline.py | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index b71079c..91b98f7 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -160,7 +160,7 @@ def pack_knots(self): def find_knot_interval(self, t): """ Find the first non-empty knot interval containing the value 't'. - i.e. knots[i] <= t <= knots[i+1], where knots[i] < knots[i+1] + i.e. knots[i] <= t < knots[i+1], where knots[i] < knots[i+1] Returns I = i - degree, which is the index of the first basis function nonzero in 't'. @@ -168,35 +168,9 @@ def find_knot_interval(self, t): :return: I """ assert self.knots[0] <= t <= self.knots[-1] + idx = np.searchsorted(self.knots, [t], side='right')[0] - 1 - self.degree + return min(idx, self.n_intervals - 1) # deals with t == self.knots[-1] - """ - This function try to find index for given t_param in knot_vec that - is covered by all (3) base functions. - :param self.knots: - :param t: - :return: - """ - - # get range without multiplicities - mn = self.knots_idx_range[0] - mx = self.knots_idx_range[1] - diff = mx - mn - - while diff > 1: - # estimate for equidistant knots - t_01 = (t - self.knots[mn]) / self.domain_size - est = int(t_01 * diff + mn) - if t > self.knots[est]: - if mn == est : - break - mn = est - else: - if mx == est: - mn = mx - break - mx = est - diff = mx - mn - return min(mn, self.knots_idx_range[1] - 1) - self.degree def _basis(self, deg, idx, t): """ diff --git a/tests/test_bspline.py b/tests/test_bspline.py index 226b0ef..9d05aa1 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -11,14 +11,22 @@ def test_find_knot_interval(self): eq_basis = bs.SplineBasis.make_equidistant(2, 100) assert eq_basis.find_knot_interval(0.0) == 0 assert eq_basis.find_knot_interval(0.001) == 0 - assert eq_basis.find_knot_interval(0.01) == 0 + assert eq_basis.find_knot_interval(0.01) == 1 assert eq_basis.find_knot_interval(0.011) == 1 assert eq_basis.find_knot_interval(0.5001) == 50 assert eq_basis.find_knot_interval(1.0 - 0.011) == 98 - assert eq_basis.find_knot_interval(1.0 - 0.01) == 98 + assert eq_basis.find_knot_interval(1.0 - 0.01) == 99 assert eq_basis.find_knot_interval(1.0 - 0.001) == 99 assert eq_basis.find_knot_interval(1.0) == 99 + knots = np.array([0, 0, 0, 0.1880192, 0.24545785, 0.51219762, 0.82239001, 1., 1. , 1.]) + basis = bs.SplineBasis(2, knots) + for interval in range(2, 7): + xx = np.linspace(knots[interval], knots[interval+1], 10) + for j, x in enumerate(xx[:-1]): + i_found = basis.find_knot_interval(x) + assert i_found == interval - 2, "i_found: {} i: {} j: {} x: {} ".format(i_found, interval-2, j, x) + def plot_basis(self, degree): eq_basis = bs.SplineBasis.make_equidistant(degree, 4) n_points = 401 @@ -37,6 +45,18 @@ def test_eval(self): #self.plot_basis(2) #self.plot_basis(3) + knots = np.array([0, 0, 0, 0.1880192, 0.24545785, 0.51219762, 0.82239001, 1., 1. , 1.]) + basis = bs.SplineBasis(2, knots) + n_points = 100 + x_coord = np.linspace(basis.domain[0], basis.domain[1], n_points) + + for i_base in range(basis.size): + y_coord = [ basis.eval(i_base, x) for x in x_coord ] + plt.plot(x_coord, y_coord) + + plt.show() + + eq_basis = bs.SplineBasis.make_equidistant(0, 2) assert eq_basis.eval(0, 0.0) == 1.0 assert eq_basis.eval(1, 0.0) == 0.0 From bd7f131133803d7e711619d96614471b9a288b5d Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Sun, 17 Sep 2017 15:35:44 +0200 Subject: [PATCH 19/36] curve approx --- src/bspline_approx.py | 17 ++++++++++++++++- tests/test_bs_approx.py | 32 +++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index bafde5a..b030326 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -9,6 +9,7 @@ #import numpy.linalg import scipy.sparse import scipy.sparse.linalg +import scipy.interpolate """ @@ -94,7 +95,7 @@ def line_3d(vtxs): -def from_grid(grid_surface, nuv, **kwargs): +def surface_from_grid(grid_surface, nuv, **kwargs): """ Make a Z_Surface of degree 2 as an approximation of the GridSurface. :param grid_surface: grid surface to approximate @@ -105,6 +106,20 @@ def from_grid(grid_surface, nuv, **kwargs): return approx.get_approximation() +def curve_from_grid(points, **kwargs): + """ + Make a Curve (of degree 3) as an approximation of a sequence of points. + :param points - N x D array, D is dimension + :param nt Prescribed number of poles of the resulting spline. + :return: Curve object. + """ + deg = kwargs.get('degree', 3) + tol = kwargs.get('tol', 0.01) + tck = scipy.interpolate.splprep(points.T, k=deg, s=tol)[0] + knots, poles, degree = tck + basis = bs.SplineBasis(degree, knots) + curve = bs.Curve(basis, np.array(poles).T) + return curve diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index 2139527..8600eb9 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -12,12 +12,13 @@ def function_sin_cos(x): class TestSurfaceApprox: + # todo: numerical test - def test_approx_grid(self): + def plot_approx_grid(self): points = bs.make_function_grid(function_sin_cos, 20, 30) gs = bs.GridSurface() gs.init_from_seq(points.reshape(-1, 3).T) - z_surf = bs_approx.from_grid(gs, (3,4) ) + z_surf = bs_approx.surface_from_grid(gs, (3,4) ) fig = plt.figure() ax = fig.gca(projection='3d') @@ -26,13 +27,13 @@ def test_approx_grid(self): plt.show() - def test_approx_transformed_grid(self): + def plot_approx_transformed_grid(self): points = bs.make_function_grid(function_sin_cos, 20, 30) mat = np.array( [ [2.0, 1.0, 0.0 ], [1.0, 2.0, 0.0 ], [0.0, 0.0, 0.5 ]]) points = np.dot(points, mat.T) + np.array([10, 20, -5.0]) gs = bs.GridSurface() gs.init_from_seq(points.reshape(-1, 3).T) - z_surf = bs_approx.from_grid(gs, (3,4) ) + z_surf = bs_approx.surface_from_grid(gs, (3,4) ) fig = plt.figure() ax = fig.gca(projection='3d') @@ -41,6 +42,23 @@ def test_approx_transformed_grid(self): plt.show() - # todo: transform grid points to uV in Gridsurface - # todo: keep quad in z_surf (OK) - # check that conversion to full keeps the transformation \ No newline at end of file + def test_surface_approx(self): + #self.plot_approx_grid() + #self.plot_approx_transformed_grid() + pass + + +class TestCurveApprox: + + def test_approx_2d(self): + x_vec = np.linspace(1.1, 3.0, 100) + y_vec = np.array([ np.sin(10*x) for x in x_vec ]) + points = np.stack( (x_vec, y_vec), axis=1) + curve = bs_approx.curve_from_grid(points) + + bs_plot.plot_curve_2d(curve, 1000) + bs_plot.plot_curve_poles_2d(curve) + + + plt.plot(x_vec, y_vec, color='green') + plt.show() \ No newline at end of file From 666b5a51a1fe07b60d916754a171c815ad18e5fd Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 18 Sep 2017 15:47:00 +0200 Subject: [PATCH 20/36] Fixes: packed_knots, simple spline extrapolation, aabb - aabb for curve - fix pock_knoots - allow evaluation of the basis out of the spline domain - allow four point Z_surface quad with linear transform - fix Z-surface transform of Z coords --- src/bspline.py | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index 91b98f7..100889a 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -132,7 +132,7 @@ def __init__(self, degree, knots): for i in range(self.degree): assert knots[i] == knots[i+1] assert knots[-i-1] == knots[-i-2] - self.knots = knots + self.knots = np.array(knots) self.size = len(self.knots) - self.degree -1 self.knots_idx_range = [self.degree, len(self.knots) - self.degree - 1] @@ -154,6 +154,7 @@ def pack_knots(self): else: packed_knots.append( (last, mult) ) last, mult = q, 1 + packed_knots.append((last,mult)) return packed_knots @@ -167,9 +168,11 @@ def find_knot_interval(self, t): :param t: float, must be within knots limits. :return: I """ - assert self.knots[0] <= t <= self.knots[-1] - idx = np.searchsorted(self.knots, [t], side='right')[0] - 1 - self.degree - return min(idx, self.n_intervals - 1) # deals with t == self.knots[-1] + idx = np.searchsorted(self.knots[self.degree: -self.degree -1], [t], side='right')[0] - 1 + if idx < 0 or idx > self.n_intervals - 1: + print("Warning: evaluation out of spline domain; t: {} min: {} max: {}".format(t, self.knots[0], self.knots[-1])) + + return max(min(idx, self.n_intervals - 1), 0) # deals with t == self.knots[-1] def _basis(self, deg, idx, t): @@ -386,6 +389,8 @@ def eval_array(self, t_points): """ return np.array( [ self.eval(t) for t in t_points] ) + def aabb(self): + return (np.amin(self.poles, axis=0), np.amax(self.poles, axis=0)) class Surface: @@ -462,6 +467,7 @@ def eval_array(self, uv_points): :param uv_points: numpy array N x [u, v] :return: Numpy array N x D, D is dimension of the curve. """ + assert uv_points.shape[1] == 2 return np.array( [ self.eval(u, v) for u, v in uv_points] ) @@ -490,10 +496,8 @@ def __init__(self, xy_quad, z_surface): self.v_basis = z_surface.v_basis self.dim = 3 - self.z_scale = 1.0 - self.z_shift = 0.0 - - if len(xy_quad) == 3: + if len(xy_quad) == 3 or \ + np.allclose(xy_quad[3], xy_quad[0] + xy_quad[2] - xy_quad[1]): # linear case self.xy_shift = xy_quad[1] v_vec = xy_quad[0] - xy_quad[1] @@ -541,12 +545,16 @@ def transform(self, xy_mat, z_mat): """ assert xy_mat.shape == (2, 3) assert z_mat.shape == (2, ) + + + self.mat_uv_to_xy = xy_mat[0:2,0:2].dot( self.mat_uv_to_xy ) self.xy_shift = xy_mat[0:2,0:2].dot( self.xy_shift ) + xy_mat[0:2, 2] self.mat_xy_to_uv = la.inv(self.mat_uv_to_xy) - self.z_scale *= z_mat[0] - self.z_shift = z_mat[0] * self.z_shift + z_mat[1] + # apply z-transfrom directly to the poles + self.z_surface.poles *= z_mat[0] + self.z_surface.poles += z_mat[1] @@ -596,6 +604,7 @@ def eval(self, u, v): def eval_array(self, uv_points): + assert uv_points.shape[1] == 2 z_points = self.z_surface.eval_array(uv_points) xy_points = self.uv_to_xy(uv_points) return np.concatenate( (xy_points, z_points), axis = 1) @@ -608,6 +617,7 @@ def eval_xy_array(self, xy_points): return np.concatenate( (xy_points, z_points), axis = 1) def z_eval_array(self, uv_points): + assert uv_points.shape[1] == 2 z_points = self.z_surface.eval_array(uv_points) return z_points.reshape(-1) @@ -623,8 +633,8 @@ class InvalidGridExc(Exception): class GridSurface: """ Surface given as bilinear interpolation of a regular grid of points. - TODO: This can be viewed as a degree 1 Z-function B-spline. Make it a common class for any degree?? """ + # TODO: calling transform lose mapping between original point grid and unit square, approximation can not be performed step_tolerance = 1e-10 def __init__(self): @@ -707,7 +717,7 @@ def init_from_seq(self, point_seq): raise InvalidGridExc("Irregular grid in Y direction, point %d"%i) #self.uv_to_xy(np.array([[0, 1], [0, 0], [1, 0], [1, 1]]).T) - self.quad = np.stack([vtx_01, vtx_00, vtx_10, vtx_11], axis = 0) + self.quad = quad = np.stack([vtx_01, vtx_00, vtx_10, vtx_11], axis = 0) # Envelope quad - polygon, oriented counter clockwise. self.grid_z = point_seq[2, :].reshape(nv, nu) @@ -725,11 +735,18 @@ def init_from_seq(self, point_seq): u_basis = SplineBasis.make_equidistant(1, nu-1) v_basis = SplineBasis.make_equidistant(1, nv-1) poles_z = np.transpose( point_seq[2, :].reshape(nv, nu, 1), axes = [1, 0, 2] ) - self.z_surface = Z_Surface(self.quad[0:3], Surface((u_basis, v_basis), poles_z) ) + self.z_surface = Z_Surface(quad[0:3], Surface((u_basis, v_basis), poles_z) ) # related bilinear Z-surface, all evaluations just call this object. + self.check_map() + def check_map(self): + # check that xy_to_uv works fine + uv_quad = self.xy_to_uv(self.quad) + print( uv_quad ) + assert np.allclose( uv_quad, np.array([[0, 1], [0, 0], [1, 0], [1, 1]]) ) - + def transform(self, xy_mat, z_mat): + self.z_surface.transform(xy_mat, z_mat) def xy_to_uv(self, xy_points): From e8aa4a4a2fa3a1cebd727491cdfb30adb41b6e1f Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 18 Sep 2017 15:47:45 +0200 Subject: [PATCH 21/36] fix plane and line approximations - try to fit end points of a curve exactely --- src/bspline_approx.py | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index b030326..e01dfb4 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -35,7 +35,8 @@ def plane_surface(vtxs): def bilinear_surface(vtxs): """ - Returns B-spline surface of a bilinear surface given by 4 corner points. + Returns B-spline surface of a bilinear surface given by 4 corner points: + uv coords: We retun also list of UV coordinates of the given points. :param vtxs: List of tuples (X,Y,Z) :return: ( Surface, vtxs_uv ) @@ -51,7 +52,7 @@ def mid(*idx): [mid(0,1), mid(0,1,2,3), mid(2,3)], [vtxs[1], mid(1,2), vtxs[2]] ] - knots = [(0.0, 3), (1.0, 3)] + knots = 3 * [0.0] + 3 * [1.0] basis = bs.SplineBasis(2, knots) surface = bs.Surface((basis, basis), poles) vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] @@ -60,40 +61,22 @@ def mid(*idx): -def _line_data(vtxs, overhang=0.0): +def line(vtxs): ''' - :param vtxs: List of tuples (X,Y) or (X,Y,Z) - :return: + Return B-spline approximation of a line from two points + :param vtxs: [ X0, X1 ], Xn are point coordinates in arbitrary dimension D + :return: Curve2D ''' assert len(vtxs) == 2 vtxs = np.array(vtxs) mid = np.mean(vtxs, axis=0) poles = [ vtxs[0], mid, vtxs[1] ] - knots = [(0.0+overhang, 3), (1.0-overhang, 3)] + knots = 3*[0.0] + 3*[1.0] basis = bs.SplineBasis(2, knots) - return (basis, poles) + return bs.Curve(basis, poles) -def line_2d(vtxs): - """ - Return B-spline approximation of line from two 2d points - :param vtxs: [ (X0, Y0), (X1, Y1) ] - :return: Curve2D - """ - return bs.Curve( *_line_data(vtxs)) - - - -def line_3d(vtxs): - """ - Return B-spline approximation of line from two 3d points - :param vtxs: [ (X0, Y0, Z0), (X1, Y1, Z0) ] - :return: Curve2D - """ - return bs.Curve(*_line_data(vtxs)) - - def surface_from_grid(grid_surface, nuv, **kwargs): """ @@ -115,7 +98,9 @@ def curve_from_grid(points, **kwargs): """ deg = kwargs.get('degree', 3) tol = kwargs.get('tol', 0.01) - tck = scipy.interpolate.splprep(points.T, k=deg, s=tol)[0] + weights = np.ones(points.shape[0]) + weights[0] = weights[-1] = 1000.0 + tck = scipy.interpolate.splprep(points.T, k=deg, s=tol, w = weights)[0] knots, poles, degree = tck basis = bs.SplineBasis(degree, knots) curve = bs.Curve(basis, np.array(poles).T) @@ -155,6 +140,7 @@ def approx_chol(self): :return: B-Spline patch """ + print('Transforming points to parametric space ...') start_time = time.time() points = self.grid_surf.points_xyz From 06c33db96017fa5919c7fedf9cf6a73bd28ce6be Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 18 Sep 2017 19:48:58 +0200 Subject: [PATCH 22/36] add plane approx test --- src/bspline_approx.py | 8 +++----- tests/test_bs_approx.py | 27 +++++++++++++++------------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index e01dfb4..e469aee 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -24,12 +24,10 @@ def plane_surface(vtxs): :return: ( Surface, vtxs_uv ) """ assert len(vtxs) == 3, "n vtx: {}".format(len(vtxs)) - vtxs.append( (0,0,0) ) vtxs = np.array(vtxs) vv = vtxs[1] + vtxs[2] - vtxs[0] vtx4 = [ vtxs[0], vtxs[1], vv, vtxs[2]] - (surf, vtxs_uv) = bilinear_surface(vtx4) - return (surf, [ vtxs_uv[0], vtxs_uv[1], vtxs_uv[3] ]) + return bilinear_surface(vtx4) @@ -55,8 +53,8 @@ def mid(*idx): knots = 3 * [0.0] + 3 * [1.0] basis = bs.SplineBasis(2, knots) surface = bs.Surface((basis, basis), poles) - vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] - return (surface, vtxs_uv) + #vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] + return surface diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index 8600eb9..0c54d5b 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -14,18 +14,20 @@ def function_sin_cos(x): class TestSurfaceApprox: # todo: numerical test + def plot_surf(self, surf): + fig = plt.figure() + ax = fig.gca(projection='3d') + bs_plot.plot_surface_3d(surf, ax) + plt.show() + + def plot_approx_grid(self): points = bs.make_function_grid(function_sin_cos, 20, 30) gs = bs.GridSurface() gs.init_from_seq(points.reshape(-1, 3).T) z_surf = bs_approx.surface_from_grid(gs, (3,4) ) + self.plot_surf(z_surf) - fig = plt.figure() - ax = fig.gca(projection='3d') - - bs_plot.plot_surface_3d(z_surf, ax) - - plt.show() def plot_approx_transformed_grid(self): points = bs.make_function_grid(function_sin_cos, 20, 30) @@ -34,20 +36,21 @@ def plot_approx_transformed_grid(self): gs = bs.GridSurface() gs.init_from_seq(points.reshape(-1, 3).T) z_surf = bs_approx.surface_from_grid(gs, (3,4) ) + self.plot_surf(z_surf) - fig = plt.figure() - ax = fig.gca(projection='3d') - - bs_plot.plot_surface_3d(z_surf, ax) - - plt.show() + def plot_plane(self): + surf = bs_approx.plane_surface([ [0, 0, 0], [1,1,0], [0,0,1] ]) + self.plot_surf(surf) def test_surface_approx(self): #self.plot_approx_grid() #self.plot_approx_transformed_grid() + self.plot_plane() pass + + class TestCurveApprox: def test_approx_2d(self): From aec6e46fd61c8ef5b5f30e347a3ed50502142644 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 18 Sep 2017 19:50:19 +0200 Subject: [PATCH 23/36] Implementation of plane and line overhang --- src/bspline.py | 8 ++++++++ src/bspline_approx.py | 29 +++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index 100889a..0907e01 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -518,6 +518,10 @@ def __init__(self, xy_quad, z_surface): else: assert False, "Three or four points must be given." + #TODO: remove this, after fixing GridSurface z_eval + self.z_scale =1.0 + self.z_shift = 1.0 + def make_full_surface(self): """ Return representation of the surface by the 3d Surface object. @@ -556,6 +560,10 @@ def transform(self, xy_mat, z_mat): self.z_surface.poles *= z_mat[0] self.z_surface.poles += z_mat[1] + #TODO: remove this, after fixing GridSurface z_eval + self.z_scale *= z_mat[0] + self.z_shift = self.z_shift*z_mat[0] + z_mat[1] + """ diff --git a/src/bspline_approx.py b/src/bspline_approx.py index e01dfb4..ec90cce 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -16,7 +16,7 @@ Approximation methods for B/splines of degree 2. """ -def plane_surface(vtxs): +def plane_surface(vtxs, overhang=0.0): """ Returns B-spline surface of a plane given by 3 points. We retun also list of UV coordinates of the given points. @@ -28,12 +28,11 @@ def plane_surface(vtxs): vtxs = np.array(vtxs) vv = vtxs[1] + vtxs[2] - vtxs[0] vtx4 = [ vtxs[0], vtxs[1], vv, vtxs[2]] - (surf, vtxs_uv) = bilinear_surface(vtx4) - return (surf, [ vtxs_uv[0], vtxs_uv[1], vtxs_uv[3] ]) + return bilinear_surface(vtx4, overhang) -def bilinear_surface(vtxs): +def bilinear_surface(vtxs, overhang=0.0): """ Returns B-spline surface of a bilinear surface given by 4 corner points: uv coords: @@ -43,6 +42,12 @@ def bilinear_surface(vtxs): """ assert len(vtxs) == 4, "n vtx: {}".format(len(vtxs)) vtxs = np.array(vtxs) + if overhang > 0.0: + inv_oh = overhang / (1.0 - 2.0 * overhang ) + dv = np.roll(vtxs, -1) - vtxs + dv *= inv_oh + vtxs += np.roll(dv, 1) - dv + def mid(*idx): return np.mean( vtxs[list(idx)], axis=0) @@ -55,13 +60,13 @@ def mid(*idx): knots = 3 * [0.0] + 3 * [1.0] basis = bs.SplineBasis(2, knots) surface = bs.Surface((basis, basis), poles) - vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] - return (surface, vtxs_uv) + #vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] + return surface -def line(vtxs): +def line(vtxs, overhang = 0.0): ''' Return B-spline approximation of a line from two points :param vtxs: [ X0, X1 ], Xn are point coordinates in arbitrary dimension D @@ -69,6 +74,11 @@ def line(vtxs): ''' assert len(vtxs) == 2 vtxs = np.array(vtxs) + if overhang > 0.0: + inv_oh = overhang / (1.0 - 2.0 * overhang) + dv = inv_oh*(vtxs[1] - vtxs[0]) + vtxs[0] -= dv + vtxs[1] += dv mid = np.mean(vtxs, axis=0) poles = [ vtxs[0], mid, vtxs[1] ] knots = 3*[0.0] + 3*[1.0] @@ -102,8 +112,11 @@ def curve_from_grid(points, **kwargs): weights[0] = weights[-1] = 1000.0 tck = scipy.interpolate.splprep(points.T, k=deg, s=tol, w = weights)[0] knots, poles, degree = tck + curve_poles=np.array(poles).T + curve_poles[0] = points[0] + curve_poles[-1] = points[-1] basis = bs.SplineBasis(degree, knots) - curve = bs.Curve(basis, np.array(poles).T) + curve = bs.Curve(basis, curve_poles) return curve From ad3af40c57bc71040ac35448e1f181d9f208102e Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Tue, 19 Sep 2017 09:33:13 +0200 Subject: [PATCH 24/36] Improve implementation of overhang. - original range of the line/plane is preserved. --- src/bspline_approx.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 0f83153..188217a 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -42,10 +42,9 @@ def bilinear_surface(vtxs, overhang=0.0): assert len(vtxs) == 4, "n vtx: {}".format(len(vtxs)) vtxs = np.array(vtxs) if overhang > 0.0: - inv_oh = overhang / (1.0 - 2.0 * overhang ) - dv = np.roll(vtxs, -1) - vtxs - dv *= inv_oh - vtxs += np.roll(dv, 1) - dv + dv = np.roll(vtxs, -1, axis=0) - vtxs + dv *= overhang + vtxs += np.roll(dv, 1, axis=0) - dv def mid(*idx): return np.mean( vtxs[list(idx)], axis=0) @@ -56,7 +55,7 @@ def mid(*idx): [mid(0,1), mid(0,1,2,3), mid(2,3)], [vtxs[1], mid(1,2), vtxs[2]] ] - knots = 3 * [0.0] + 3 * [1.0] + knots = 3 * [0.0 - overhang] + 3 * [1.0 + overhang] basis = bs.SplineBasis(2, knots) surface = bs.Surface((basis, basis), poles) #vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] @@ -74,13 +73,12 @@ def line(vtxs, overhang = 0.0): assert len(vtxs) == 2 vtxs = np.array(vtxs) if overhang > 0.0: - inv_oh = overhang / (1.0 - 2.0 * overhang) - dv = inv_oh*(vtxs[1] - vtxs[0]) + dv = overhang*(vtxs[1] - vtxs[0]) vtxs[0] -= dv vtxs[1] += dv mid = np.mean(vtxs, axis=0) poles = [ vtxs[0], mid, vtxs[1] ] - knots = 3*[0.0] + 3*[1.0] + knots = 3*[0.0 - overhang] + 3*[1.0 + overhang] basis = bs.SplineBasis(2, knots) return bs.Curve(basis, poles) From 1525d147f925f0b277964ce527946ad155f0645a Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 20 Sep 2017 11:21:05 +0200 Subject: [PATCH 25/36] Improve bspline - cleanup of bspline, GridSurface (WIP) - added tests for bspline - dev notes fro bs_approx --- src/bspline.py | 611 +++++++++++++++++++++++++--------------- src/bspline_approx.py | 19 ++ src/bspline_plot.py | 13 +- tests/test_bs_approx.py | 8 +- tests/test_bspline.py | 107 ++++--- 5 files changed, 483 insertions(+), 275 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index 0907e01..4219ad8 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -1,20 +1,16 @@ """ -Module with classes representing various B-spline and NURMS curves and surfaces. +Module with classes representing various B-spline and NURBS curves and surfaces. These classes provide just basic functionality: - storing the data - evaluation of XYZ for UV -In future: - evaluation and xy<->uv functions accepting np.arrays, -- serialization and deserialization using JSONdata - must make it an installable module -- use de Boor algorithm for evaluation of curves and surfaces - evaluation of derivatives +In future: +- use de Boor algorithm for evaluation of curves and surfaces +- serialization and deserialization using JSONdata - must make it an installable module - implement degree increasing and knot insertion """ -""" -This module tries to approximate 2.5D array of terrain points -using B-Spline surface. -""" import matplotlib.pyplot as plt import numpy as np @@ -25,55 +21,55 @@ __author__ = 'Jan Brezina , Jiri Hnidek , Jiri Kopal ' -class ParamError(Exception): - pass - -def check_matrix(mat, shape, values, idx=[]): - ''' - Check shape and type of scalar, vector or matrix. - :param mat: Scalar, vector, or vector of vectors (i.e. matrix). Vector may be list or other iterable. - :param shape: List of dimensions: [] for scalar, [ n ] for vector, [n_rows, n_cols] for matrix. - If a value in this list is None, the dimension can be arbitrary. The shape list is set fo actual dimensions - of the matrix. - :param values: Type or tuple of allowed types of elements of the matrix. E.g. ( int, float ) - :param idx: Internal. Used to pass actual index in the matrix for possible error messages. - :return: - ''' - try: - if len(shape) == 0: - if not isinstance(mat, values): - raise ParamError("Element at index {} of type {}, expected instance of {}.".format(idx, type(mat), values)) - else: - if shape[0] is None: - shape[0] = len(mat) - l=None - if not hasattr(mat, '__len__'): - l=0 - elif len(mat) != shape[0]: - l=len(mat) - if not l is None: - raise ParamError("Wrong len {} of element {}, should be {}.".format(l, idx, shape[0])) - for i, item in enumerate(mat): - sub_shape = shape[1:] - check_matrix(item, sub_shape, values, idx = [i] + idx) - shape[1:] = sub_shape - return shape - except ParamError: - raise - except Exception as e: - raise ParamError(e) - - -def check_knots(deg, knots, N): - total_multiplicity = 0 - for knot, mult in knots: - # This condition must hold if we assume only (0,1) interval of curve or surface parameters. - #assert float(knot) >= 0.0 and float(knot) <= 1.0 - total_multiplicity += mult - assert total_multiplicity == deg + N + 1 - - -scalar_types = (int, float, np.int64) +# class ParamError(Exception): +# pass +# +# def check_matrix(mat, shape, values, idx=[]): +# ''' +# Check shape and type of scalar, vector or matrix. +# :param mat: Scalar, vector, or vector of vectors (i.e. matrix). Vector may be list or other iterable. +# :param shape: List of dimensions: [] for scalar, [ n ] for vector, [n_rows, n_cols] for matrix. +# If a value in this list is None, the dimension can be arbitrary. The shape list is set fo actual dimensions +# of the matrix. +# :param values: Type or tuple of allowed types of elements of the matrix. E.g. ( int, float ) +# :param idx: Internal. Used to pass actual index in the matrix for possible error messages. +# :return: +# ''' +# try: +# if len(shape) == 0: +# if not isinstance(mat, values): +# raise ParamError("Element at index {} of type {}, expected instance of {}.".format(idx, type(mat), values)) +# else: +# if shape[0] is None: +# shape[0] = len(mat) +# l=None +# if not hasattr(mat, '__len__'): +# l=0 +# elif len(mat) != shape[0]: +# l=len(mat) +# if not l is None: +# raise ParamError("Wrong len {} of element {}, should be {}.".format(l, idx, shape[0])) +# for i, item in enumerate(mat): +# sub_shape = shape[1:] +# check_matrix(item, sub_shape, values, idx = [i] + idx) +# shape[1:] = sub_shape +# return shape +# except ParamError: +# raise +# except Exception as e: +# raise ParamError(e) +# +# +# def check_knots(deg, knots, N): +# total_multiplicity = 0 +# for knot, mult in knots: +# # This condition must hold if we assume only (0,1) interval of curve or surface parameters. +# #assert float(knot) >= 0.0 and float(knot) <= 1.0 +# total_multiplicity += mult +# assert total_multiplicity == deg + N + 1 +# +# +# scalar_types = (int, float, np.int64) @@ -100,9 +96,9 @@ def make_equidistant(cls, degree, n_intervals, knot_range=[0.0, 1.0]): Returns spline basis for an eqidistant knot vector having 'n_intervals' subintervals. :param degree: degree of the spline basis - :param n_intervals: length of vector + :param n_intervals: Number of subintervals. :param knot_range: support of the spline, min and max valid 't' - :return: np array of knots + :return: SplineBasis object. """ n = n_intervals + 2 * degree + 1 knots = np.array((knot_range[0],) * n) @@ -115,37 +111,57 @@ def make_equidistant(cls, degree, n_intervals, knot_range=[0.0, 1.0]): @classmethod def make_from_packed_knots(cls, degree, knots): + """ + Construct basis from the vector of packed knots. + :param degree: Degree of the basis. + :param knots: List of knots with their multiplicities, [ (knot, mult), ..] + :return: SplineBasis object. + """ full_knots = [ q for q, mult in knots for i in range(mult) ] return cls(degree, full_knots) def __init__(self, degree, knots): """ - Constructor of the basis. + Constructor of the spline basis. :param degree: Degree of Bezier polynomials >=0. :param knots: Numpy array of the knots including multiplicities. """ assert degree >=0 self.degree = degree - # check free ends (and full degree along the whole curve) + # check free ends for i in range(self.degree): assert knots[i] == knots[i+1] assert knots[-i-1] == knots[-i-2] self.knots = np.array(knots) self.size = len(self.knots) - self.degree -1 + # Number of basis functions. + + self.knots_idx_range = [self.degree, len(self.knots) - self.degree - 1] + # Range of knot indices corrsponding to the basis domain. + self.domain = self.knots[self.knots_idx_range] + # Support domain of the spline. + self.domain_size = self.domain[1] - self.domain[0] + # Size of the domain. + self.n_intervals = self.size - self.degree - # Number of basis functions. + # Number of subintervals ( assuming multiplicities only on ends. ) + # Set optimized functions for specific degrees. if self.degree == 2: - self.eval_base_vector = self._eval_base_vector_deg_2 - self.eval_diff_base_vector = self._eval_diff_base_vector_deg_2 + self.eval_base_vector = self._eval_vector_deg_2 + self.eval_diff_base_vector = self._eval_diff_vector_deg_2 + def pack_knots(self): + """ + :return: Packed knot vector, [ (knot, multiplicity), .. ] + """ last, mult = self.knots[0], 0 packed_knots = [] for q in self.knots: @@ -169,10 +185,8 @@ def find_knot_interval(self, t): :return: I """ idx = np.searchsorted(self.knots[self.degree: -self.degree -1], [t], side='right')[0] - 1 - if idx < 0 or idx > self.n_intervals - 1: - print("Warning: evaluation out of spline domain; t: {} min: {} max: {}".format(t, self.knots[0], self.knots[-1])) - - return max(min(idx, self.n_intervals - 1), 0) # deals with t == self.knots[-1] + assert 0 <= idx <= self.n_intervals, "Evaluation out of spline domain; t: {} min: {} max: {}".format(t, self.knots[0], self.knots[-1]) + return min(idx, self.n_intervals - 1) # deals with t == self.knots[-1] def _basis(self, deg, idx, t): @@ -250,7 +264,6 @@ def eval_diff(self, i_base, t): return diff - def make_linear_poles(self): """ Return poles of basis functions to get a f(x) = x. @@ -263,33 +276,51 @@ def make_linear_poles(self): return poles - def eval_base_vector(self, i_base, t): + def eval_vector(self, i_base, t): + """ + This function compute base function of B-Spline curve on given subinterval. + :param i_int: Interval in which 't' belongs. Three nonzero basis functions on this interval are evaluated. + :param t: Where to evaluate. + :return: Numpy array of three values. + """ values = [] for ib in range(i_base, i_base + self.degree + 1): values.append( self.eval(ib, t)) return values - def eval_diff_base_vector(self, i_base, t): + def eval_diff_vector(self, i_base, t): + """ + This function compute derivative of base function of B-Spline curve on given subinterval. + :param i_int: Interval in which 't' belongs. Derivatives of the 3 nonzero basis functions on this interval are evaluated. + :param t: Where to evaluate. + :return: Numpy array of three values. + """ values = [] for ib in range(i_base, i_base + self.degree + 1): values.append( self.eval_diff(ib, t)) return values - def _eval_base_vector_deg_2(self, i_base, t): + """ + Specializations. + TODO: + - Try usage of scipy evaluation, compare speed with optimized eval_vector and diff_eval_vector. + - Generalize optimized evaluation of eval_vector (If scipy is not faster), De Boor algortihm, Hoschek 4.3.3. + - Optimize eval and eval_diff - without recursion, based on combinatorGeneralize optimized evaluation of eval_vector (If scipy is not faster) + """ + def _eval_vector_deg_2(self, i_int, t): """ - This function compute normalized blending function aka base function of B-Spline curve or surface. - :param knot_vec: - :param t_param: - :param order: (0: function value, 1: derivative function value) - :param sparse: - :return: + This function compute base function of B-Spline curve on given subinterval. + :param i_int: Interval in which 't' belongs. Three nonzero basis functions on this interval are evaluated. + :param t: Where to evaluate. + :return: Numpy array of three values. + Note: Keep code redundancy with 'diff' as optimization. """ basis_values = np.zeros(3) - tk1, tk2, tk3, tk4 = self.knots[i_base + 1 : i_base + 5] + tk1, tk2, tk3, tk4 = self.knots[i_int + 1 : i_int + 5] d31 = tk3 - tk1 d32 = tk3 - tk2 @@ -310,11 +341,18 @@ def _eval_base_vector_deg_2(self, i_base, t): return basis_values - def _eval_diff_base_vector_deg_2(self, i_base, t): + def _eval_diff_vector_deg_2(self, i_int, t): + """ + This function compute derivative of base function of B-Spline curve on given subinterval. + :param i_int: Interval in which 't' belongs. Derivatives of the 3 nonzero basis functions on this interval are evaluated. + :param t: Where to evaluate. + :return: Numpy array of three values. + Note: Keep code redundancy with 'diff' as optimization. + """ basis_values = np.zeros(3) - tk1, tk2, tk3, tk4 = self.knots[i_base + 1: i_base + 5] + tk1, tk2, tk3, tk4 = self.knots[i_int + 1: i_int + 5] d31 = tk3 - tk1 d32 = tk3 - tk2 @@ -336,39 +374,60 @@ def _eval_diff_base_vector_deg_2(self, i_base, t): class Curve: + """ + Defines a D-dim B-spline curve. + """ @classmethod def make_raw(cls, poles, knots, rational=False, degree=2): """ Construct a B-spline curve. - :param poles: List of poles (control points) ( X, Y, Z ) or weighted points (X,Y,Z, w). X,Y,Z,w are floats. - Weighted points are used only for rational B-splines (i.e. nurbs) - :param knots: List of tuples (knot, multiplicity), where knot is float, t-parameter on the curve of the knot - and multiplicity is positive int. Total number of knots, i.e. sum of their multiplicities, must be - degree + N + 1, where N is number of poles. + :param poles: Numpy array N x (D+r) of poles (control points). N is number of poles, D is dimension of the curve, 'r' is 1 for rational curves. + For rational case, poles[:, D] are weights of the control points. + :param knots, degree: See SplineBasis. :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. :param degree: Non-negative int """ basis = SplineBasis(degree, knots) return cls(basis, poles, rational) - """ - Defines a 3D (or (dim -D) curve as B-spline. We shall work only with B-splines of degree 2. - Corresponds to "B-spline Curve - <3D curve record 7>" from BREP format description. - """ def __init__(self, basis, poles, rational = False): + """ + Construct a B-spline curve. + :param poles: Numpy array N x (D+r) of poles (control points). N is number of poles, D is dimension of the curve, 'r' is 1 for rational curves. + For rational case, poles[:, D] are weights of the control points. + :param basis: SplineBasis object. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. + """ + self.basis = basis + # Spline basis. + + self.poles = np.array(poles, dtype=float) # N x D + assert self.poles.shape[0] == self.basis.size + # Spline poles. + self.dim = len(poles[0]) - rational - check_matrix(poles, [self.basis.size, self.dim + rational], scalar_types ) + # Dimension of the curve. + + self.rational = rational + # Indicator of rational B-spline (NURBS). - self.poles=np.array(poles) # N x D - self.rational=rational if rational: + # precomputations self._weights = poles[:, self.dim] self._poles = (poles[:, 0:self.dim].T * self._weights ).T def eval(self, t): + """ + Evaluate a B-spline curve for paramater 't'. + :param t: Evaluation point. + :return: D-dimensional pnumpy array. D - is dimension given by dimension of poles. + TODO: + - use basis.eval_vector + - test evaluation for rational curves + """ it = self.basis.find_knot_interval(t) dt = self.basis.degree + 1 @@ -390,12 +449,16 @@ def eval_array(self, t_points): return np.array( [ self.eval(t) for t in t_points] ) def aabb(self): + """ + Return Axes Aligned Bounding Box of the poles, which should be also bounding box of the curve itself. + :return: ( min_corner, max_corner); Box corners are numpy arryas of dimension D. + """ return (np.amin(self.poles, axis=0), np.amax(self.poles, axis=0)) class Surface: """ - Defines a B-spline surface. + Defines D-dim B-spline surface. """ @classmethod @@ -417,30 +480,41 @@ def make_raw(cls, poles, knots, rational=False, degree=(2,2)): def __init__(self, basis, poles, rational=False): """ - Construct a B-spline in 3d space. - :param poles: Matrix (list of lists) of Nu times Nv poles (control points). - Single pole is a points ( X, Y, Z ) or weighted point (X,Y,Z, w). X,Y,Z,w are floats. - Weighted points are used only for rational B-splines (i.e. nurbs) - :param knots: Tuple (u_knots, v_knots). Both u_knots and v_knots are lists of tuples - (knot, multiplicity), where knot is float, t-parameter on the curve of the knot - and multiplicity is positive int. For both U and V knot vector the total number of knots, - i.e. sum of their multiplicities, must be degree + N + 1, where N is number of poles. - :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. BREP format have two independent flags - for U and V parametr, but only choices 0,0 and 1,1 have sense. - :param degree: (u_degree, v_degree) Both positive ints. + Construct a B-spline surface. + :param poles: Numpy array Nu x Nv x (D+r) of poles (control points). + Nu and Nv are sizes of u_basis, v_basis respectively. + D is dimension of the surface, 'r' is 1 for rational surfaces. + For rational case, poles[:, :, D] are weights of the control points. + :param basis: (u_basis, v_basis) SplineBasis objects for U and V parameter axis. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. """ - self.u_basis, self.v_basis = basis - self.rational = rational + # Surface basis for U and V axis. + self.dim = len(poles[0][0]) - rational - check_matrix(poles, [self.u_basis.size, self.v_basis.size, self.dim + rational], scalar_types ) - self.poles=np.array(poles) + # Surface dimension, D. + + self.poles=np.array(poles, dtype=float) + # Surface poles matrix: Nu x Nv x (D+r) assert self.poles.shape == (self.u_basis.size, self.v_basis.size, self.dim + rational) + + self.rational = rational + # Rational surface indicator. if rational: + # precomputations self._weights = poles[:, :, self.dim] self._poles = (poles[:,:,0:self.dim].T * self._weights.T ).T def eval(self, u, v): + """ + Evaluate a B-spline surface for paramaters u,v. + :param u, v: Evaluation point. + :return: D-dimensional numpy array. D - is dimension given by dimension of poles. + TODO: + - use basis.eval_vector + - test evaluation for rational curves + """ + iu = self.u_basis.find_knot_interval(u) iv = self.v_basis.find_knot_interval(v) du = self.u_basis.degree + 1 @@ -463,9 +537,9 @@ def eval(self, u, v): def eval_array(self, uv_points): """ - Evaluate in array of t-points. + Evaluate in array of uv-points. :param uv_points: numpy array N x [u, v] - :return: Numpy array N x D, D is dimension of the curve. + :return: Numpy array N x D; D is dimension of the curve. """ assert uv_points.shape[1] == 2 return np.array( [ self.eval(u, v) for u, v in uv_points] ) @@ -474,53 +548,59 @@ def eval_array(self, uv_points): class Z_Surface: """ - Simplified B-spline surface that use just linear or bilinear transform between XY and UV. - - TODO: - - We need conversion to full 3D surface for the BREP output - - Optimization: simplified Bspline evaluation just for the singel coordinate + Simplified B-spline surface that use just linear or bilinear transform between XY and UV. """ def __init__(self, xy_quad, z_surface): """ - Construct a surface given by the 1d surface for the Z coordinate and XY quadrilateral - for the bilinear UV -> XY mapping. + Construct a surface given by the 1d surface for the Z coordinate and XY quadrilateral + for the bilinear UV <-> XY mapping. :param xy_quad: np array N x 2 Four or three points, determining bilinear or linear mapping, respectively. Four points giving XY coordinates for the uv corners: (0,1), (0,0), (1,0), (1,1) Three points giving XY coordinates for the uv corners: (0,1), (0,0), (1,0) - :param z_surface: !D Surface object. + Linear case is also detected for the four points. + :param z_surface: 1D Surface object. """ assert z_surface.dim == 1 + self.dim = 3 + # Fixed surface dimension. + self.z_surface = z_surface + # Underlaying 1d surface object for Z coord evaluation. + self.u_basis = z_surface.u_basis self.v_basis = z_surface.v_basis - self.dim = 3 + # Basis for UV directions. + + # Build envelope quadrilateral polygon in XY plane. + self.quad = np.array(xy_quad, dtype=float) + assert self.quad.shape[0] in [3, 4], "Three or four points must be given." + assert self.quad.shape[1] == 2 + + v11 = self.quad[0] + self.quad[2] - self.quad[1] + if self.quad.shape[0] == 3: + self.quad = np.concatenate( (self.quad, v11[None, :]), axis = 0) - if len(xy_quad) == 3 or \ - np.allclose(xy_quad[3], xy_quad[0] + xy_quad[2] - xy_quad[1]): + if np.allclose(self.quad[3], v11): # linear case - self.xy_shift = xy_quad[1] - v_vec = xy_quad[0] - xy_quad[1] - u_vec = xy_quad[2] - xy_quad[1] - self.mat_uv_to_xy = np.column_stack((u_vec, v_vec)) - self.mat_xy_to_uv = la.inv(self.mat_uv_to_xy) + self._xy_shift = self.quad[1] + v_vec = self.quad[0] - self.quad[1] + u_vec = self.quad[2] - self.quad[1] + self._mat_uv_to_xy = np.column_stack((u_vec, v_vec)) + self._mat_xy_to_uv = la.inv(self._mat_uv_to_xy) self.xy_to_uv = self._linear_xy_to_uv self.uv_to_xy = self._linear_uv_to_xy - elif len(xy_quad) == 4: + else: # bilinear case - self.quad = xy_quad - self.xy_to_uv = self._bilinear_xy_to_uv self.uv_to_xy = self._bilinear_uv_to_xy - else: - assert False, "Three or four points must be given." #TODO: remove this, after fixing GridSurface z_eval - self.z_scale =1.0 - self.z_shift = 1.0 + #self.z_scale =1.0 + #self.z_shift = 1.0 def make_full_surface(self): """ @@ -540,9 +620,9 @@ def make_full_surface(self): return Surface(basis, poles) - def transform(self, xy_mat, z_mat): + def transform(self, xy_mat, z_mat=np.array( [1.0, 0.0] ) ): """ - Transform the surface by arbitrary XY linear transform and Z linear transform. + Transform the Z-surface by arbitrary XY linear transform and Z linear transform. :param xy_mat: np array, 2 rows 3 cols, last column is xy shift :param z_shift: [ z_scale, z_shift] :return: None @@ -550,19 +630,17 @@ def transform(self, xy_mat, z_mat): assert xy_mat.shape == (2, 3) assert z_mat.shape == (2, ) - - - self.mat_uv_to_xy = xy_mat[0:2,0:2].dot( self.mat_uv_to_xy ) - self.xy_shift = xy_mat[0:2,0:2].dot( self.xy_shift ) + xy_mat[0:2, 2] - self.mat_xy_to_uv = la.inv(self.mat_uv_to_xy) + self._mat_uv_to_xy = xy_mat[0:2,0:2].dot( self._mat_uv_to_xy ) + self._xy_shift = xy_mat[0:2,0:2].dot( self._xy_shift ) + xy_mat[0:2, 2] + self._mat_xy_to_uv = la.inv(self._mat_uv_to_xy) # apply z-transfrom directly to the poles self.z_surface.poles *= z_mat[0] self.z_surface.poles += z_mat[1] #TODO: remove this, after fixing GridSurface z_eval - self.z_scale *= z_mat[0] - self.z_shift = self.z_shift*z_mat[0] + z_mat[1] + #self.z_scale *= z_mat[0] + #self.z_shift = self.z_shift*z_mat[0] + z_mat[1] @@ -575,7 +653,7 @@ def uv_to_xy(self, uv_points): """ def _linear_uv_to_xy(self, uv_points): assert uv_points.shape[1] == 2, "Size: {}".format(uv_points.shape) - return ( np.dot(uv_points, self.mat_uv_to_xy.T) + self.xy_shift) + return ( np.dot(uv_points, self._mat_uv_to_xy.T) + self._xy_shift) def _bilinear_uv_to_xy(self, uv_points): @@ -596,7 +674,7 @@ def xy_to_uv(self, xy_points): def _linear_xy_to_uv(self, xy_points): # assert xy_points.shape[0] == 2 assert xy_points.shape[1] == 2 - return np.dot((xy_points - self.xy_shift), self.mat_xy_to_uv.T) + return np.dot((xy_points - self._xy_shift), self._mat_xy_to_uv.T) def _bilinear_xy_to_uv(self, xy_points): @@ -605,6 +683,11 @@ def _bilinear_xy_to_uv(self, xy_points): def eval(self, u, v): + """ + Evaluate a B-spline surface for paramaters u,v. + :param u, v: Evaluation point. + :return: D-dimensional numpy array. D - is dimension given by dimension of poles. + """ z = self.z_surface.eval(u, v) uv_points = np.array([[u, v]]) x, y = self.uv_to_xy( uv_points )[0] @@ -612,149 +695,215 @@ def eval(self, u, v): def eval_array(self, uv_points): + """ + Evaluate a B-spline surface in array of UV points. + :param uv_points: numpy array N x [u, v] + :return: array N x D; D - is dimension given by dimension of poles. + """ assert uv_points.shape[1] == 2 z_points = self.z_surface.eval_array(uv_points) xy_points = self.uv_to_xy(uv_points) return np.concatenate( (xy_points, z_points), axis = 1) - def eval_xy_array(self, xy_points): + """ + Evaluate a B-spline surface in array of XY points. + :param xy_points: numpy array N x [x, y] + :return: array N x D; D - is dimension given by dimension of poles. + """ uv_points = self.xy_to_uv(xy_points) z_points = self.z_surface.eval_array(uv_points) return np.concatenate( (xy_points, z_points), axis = 1) + def z_eval_array(self, uv_points): + """ + Evaluate just Z coordinate for array of UV points. + :param uv_points: numpy array N x [u, v] + :return: array N x D; D - is dimension given by dimension of poles. + """ assert uv_points.shape[1] == 2 z_points = self.z_surface.eval_array(uv_points) return z_points.reshape(-1) + def z_eval_xy_array(self, xy_points): + """ + Evaluate just Z coordinate for array of XY points. + :param uv_points: numpy array N x [x, y] + :return: array N x D; D - is dimension given by dimension of poles. + """ uv_points = self.xy_to_uv(xy_points) z_points = self.z_surface.eval_array(uv_points) return z_points.reshape(-1) -class InvalidGridExc(Exception): + + + +class GridNotInShapeExc(Exception): pass +class IrregularGridExc(Exception): + pass -class GridSurface: - """ - Surface given as bilinear interpolation of a regular grid of points. - """ - # TODO: calling transform lose mapping between original point grid and unit square, approximation can not be performed - step_tolerance = 1e-10 - def __init__(self): - """ - Initialize point grid from numpy array. - :param grid: NxMx3 numpy array of NxM grid of #D coordinates - """ - self.grid=None - self.mat_xy_to_uv=None - self.mat_uv_to_xy=None - self.shift=None - self.shape = (0,0) - self.uv_step = (0,0) +class GridSurface: + step_tolerance = 1e-5 + # relative step_tolerance of + @staticmethod def load(self, filename): """ Load the grid surface from file :param filename: - :return: + :return: GridSurface object. """ point_seq = np.loadtxt(filename) assert min(point_seq.shape) > 1 - self.init_from_seq(point_seq.T) + GridSurface(point_seq.T) + """ + Can load and check grid of XYZ points and construct a + Z-surface of degree 1 for them. + """ + # TODO: calling transform lose mapping between original point grid and unit square, approximation can not be performed + - def init_from_seq(self, point_seq): + def __init__(self, point_seq, tolerance = 1e-6): """ - Get 2d transform matrix 2 rows 3 cols to map a grid of XY points to unit square - :param point_seq: numpy array N x 2 - :return: + Construct the GridSurface from sequence of points. + :param point_seq: N x 3 numpy array; organized as Nu x Nv grid. + Nu, Nv are detected automaticaly, both must be greater then 1. + :param step_tolerance: Tolerance between XY position given by envelope quad and actual point position. + Relative to the length of maximal side of the envelope quad. """ + self.tolerance = tolerance + + assert point_seq.shape[1] == 3 + n_points = point_seq.shape[0] + point_seq_xy = point_seq[:, 0:2] - assert point_seq.shape[0] == 3 - n_points = point_seq.shape[1] - point_seq_xy = point_seq[0:2,:] + self.quad = None + # Envelope quad - polygon, oriented counter clockwise. - vtx_00 = point_seq_xy[0:2, 0] - vtx_du = point_seq_xy[0:2, 1] + self.shape = None + # Number of points along axis: Nu x Nv - u_step = vtx_du - vtx_00 - for i in range(2, n_points): - step = point_seq_xy[:,i] - point_seq_xy[:,i-1] - if la.norm(u_step - step) > self.step_tolerance: - break + self._get_grid_corners(point_seq_xy) + self._check_grid_regularity(point_seq_xy) - vtx_dv = point_seq_xy[:,i] - v_step = vtx_dv - vtx_00 + self.z_scale = 1.0 + self.z_shift = 0.0 - nu = i - nv = int(n_points / nu) - if not n_points == nu*nv: - raise InvalidGridExc("Not a M*N grid.") + # self._grid_z = point_seq[2, :].reshape(self.nv, self.nu) + # grid of original Z values, format as matrix - # check total range of the grid - vtx_10 = point_seq_xy[:, nu-1] - vtx_01 = point_seq_xy[:, -nu] - vtx_11 = point_seq_xy[:, -1] - u_range_0 = vtx_10 - vtx_00 - u_range_1 = vtx_11 - vtx_01 - v_range_0 = vtx_01 - vtx_00 - v_range_1 = vtx_11 - vtx_10 - if not la.norm(u_range_0 - u_range_1) < self.step_tolerance or \ - not la.norm(v_range_0 - v_range_1) < self.step_tolerance: - raise InvalidGridExc("Grid XY envelope is not a parallelogram.") + self._uv_step = (1.0 / float(self.shape[0] - 1), 1.0 / float(self.shape[1] - 1)) + # Grid step in u, v direction respectively. - u_step = u_range_0 / (nu-1) - v_step = v_range_0 / (nv-1) + self._make_z_surface( point_seq) - # check regularity of the grid - for i in range(nu*nv): - pred_x = i - 1 - pred_y = i - nu - if i%nu == 0: - pred_x= -1 - if pred_x > 0 and not la.norm(point_seq_xy[:, i] - point_seq_xy[:, pred_x] - u_step) < 2*self.step_tolerance: - raise InvalidGridExc("Irregular grid in X direction, point %d"%i) - if pred_y > 0 and not la.norm(point_seq_xy[:, i] - point_seq_xy[:, pred_y] - v_step) < 2*self.step_tolerance: - raise InvalidGridExc("Irregular grid in Y direction, point %d"%i) - - #self.uv_to_xy(np.array([[0, 1], [0, 0], [1, 0], [1, 1]]).T) - self.quad = quad = np.stack([vtx_01, vtx_00, vtx_10, vtx_11], axis = 0) - # Envelope quad - polygon, oriented counter clockwise. + self.points_xyz = point_seq + # Original sequance of XYZ points - self.grid_z = point_seq[2, :].reshape(nv, nu) - # grid of original Z values, format as matrix + self._check_map() + + def _get_grid_corners(self, xy_points): + n_points = len(xy_points) + vtx_00 = xy_points[0, 0:2] + vtx_dv = xy_points[1, 0:2] + + # detect grid shape + v_step = vtx_dv - vtx_00 + step_tolerance = self.tolerance * la.norm(v_step, np.inf) + for i in range(2, n_points): + step = xy_points[i,:] - xy_points[i - 1, :] + if la.norm(v_step - step, np.inf) > step_tolerance: + break + else: + raise GridNotInShapeExc("End of the first row not detected.") + nv = i + nu = int(n_points / nv) + if not n_points == nu * nv: + raise GridNotInShapeExc("Not a Nu x Nv grid.") self.shape = (nu, nv) - # Grid shape. - self.uv_step = (1.0 / float(nu - 1), 1.0 / float(nv - 1)) - # Grid step in u, v direction respectively. + # make envelope quad + vtx_01 = xy_points[nv - 1, :] + vtx_10 = xy_points[-nv, :] + vtx_11 = xy_points[-1, :] + self.quad = np.array( [ vtx_01, vtx_00, vtx_10, vtx_11 ], dtype = float ) - self.points_xyz = point_seq.T - # Original sequance of XYZ points + # check that quad is parallelogram. + diff = np.roll(self.quad, -1, axis = 0) - self.quad + if not la.norm(diff[0] + diff[2]) < self.tolerance * la.norm(diff[0]) or \ + not la.norm(diff[1] + diff[3]) < self.step_tolerance * la.norm(diff[1]): + raise GridNotInShapeExc("Grid XY envelope is not a parallelogram.") + + self._point_tol = np.max( la.norm(diff, axis = 1) ) + self._u_step = diff[1] / (nu-1) # v10 - v00 + self._v_step = diff[2] / (nv-1) # v11 - v10 + + + def _check_grid_regularity(self, points_xy): + # check regularity of the grid + nu, nv = self.shape + for i in range(nu * nv): + pred_y = i - 1 + pred_x = i - nv + if i%nv == 0: + pred_y = -1 + if pred_x > 0 and not la.norm(points_xy[i, :] - points_xy[pred_x, :] - self._u_step) < self._point_tol: + raise IrregularGridExc("Irregular grid in X direction, point %d"%i) + if pred_y > 0 and not la.norm(points_xy[i, :] - points_xy[pred_y, :] - self._v_step) < self._point_tol: + raise IrregularGridExc("Irregular grid in Y direction, point %d"%i) + + def _make_z_surface(self, points): + nu, nv = self.shape + u_basis = SplineBasis.make_equidistant(1, nu - 1) + v_basis = SplineBasis.make_equidistant(1, nv - 1) + + + + poles_z = points[:, 2].reshape(nu, nv, 1) + self.z_surface = Z_Surface(self.quad[0:3], Surface((u_basis, v_basis), poles_z) ) + uv_points = self.z_surface.xy_to_uv( points[:, 0:2] ) + grid_uv = uv_points.reshape(nu, nv, 2) + self.grid_uvz = np.concatenate((grid_uv, poles_z), axis=2) - u_basis = SplineBasis.make_equidistant(1, nu-1) - v_basis = SplineBasis.make_equidistant(1, nv-1) - poles_z = np.transpose( point_seq[2, :].reshape(nv, nu, 1), axes = [1, 0, 2] ) - self.z_surface = Z_Surface(quad[0:3], Surface((u_basis, v_basis), poles_z) ) # related bilinear Z-surface, all evaluations just call this object. - self.check_map() - def check_map(self): + + def _check_map(self): # check that xy_to_uv works fine uv_quad = self.xy_to_uv(self.quad) print( uv_quad ) assert np.allclose( uv_quad, np.array([[0, 1], [0, 0], [1, 0], [1, 1]]) ) + + + def transform(self, xy_mat, z_mat): - self.z_surface.transform(xy_mat, z_mat) + """ + Transform the GridSurface by arbitrary XY linear transform and Z linear transform. + :param xy_mat: np array, 2 rows 3 cols, last column is xy shift + :param z_shift: [ z_scale, z_shift] + :return: None + Note: + """ + # just XY transfrom of underlaying z_surface + self.z_surface.transform(xy_mat) + + # transform quad + self.quad = self.z_surface.uv_to_xy( np.array([[0, 1], [0, 0], [1, 0], [1, 1]]) ) + + # transform z_scale + self.z_scale = 1.0 + self.z_shift = 0.0 def xy_to_uv(self, xy_points): @@ -799,16 +948,16 @@ def z_eval_array(self, uv_points): result = np.zeros(uv_points.shape[0]) for i, uv in enumerate(uv_points): - iuv = np.floor(uv / self.uv_step) + iuv = np.floor(uv / self._uv_step) iu = max(0, min(self.shape[0] - 2, int(iuv[0]))) iv = max(0, min(self.shape[1] - 2, int(iuv[1]))) iuv = np.array([iu, iv]) - uv_loc = uv / self.uv_step - iuv + uv_loc = uv / self._uv_step - iuv u_loc = np.array([1 - uv_loc[0], uv_loc[0]]) v_loc = np.array([1 - uv_loc[1], uv_loc[1]]) - Z_mat = self.grid_z[iv: (iv + 2), iu: (iu + 2)] - result[i] = self.z_surface.z_scale*(v_loc.dot(Z_mat).dot(u_loc)) + self.z_surface.z_shift + Z_mat = self.grid_uvz[iu: (iu + 2), iv: (iv + 2), 2] + result[i] = self.z_scale*(v_loc.dot(Z_mat).dot(u_loc)) + self.z_shift return result diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 188217a..2b01a8c 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -102,6 +102,15 @@ def curve_from_grid(points, **kwargs): :param points - N x D array, D is dimension :param nt Prescribed number of poles of the resulting spline. :return: Curve object. + + TODO: + - Measure efficiency. Estimate how good we can be. Do it our self if we can do at leas 10 times better. + - Find out which method is used. Hoschek (4.4.1) refers to several methods how to determine parametrization of + the curve, i.e. Find parameters t_i to the given approximation points P_i. + - Further on it is not clear what is the mening of the 's' parameter and how one cna influence tolerance and smoothness. + - Some sort of adaptivity is used. + + """ deg = kwargs.get('degree', 3) tol = kwargs.get('tol', 0.01) @@ -122,6 +131,16 @@ def curve_from_grid(points, **kwargs): class _SurfaceApprox: + """ + TODO: + - Check efficiency of scipy methods, compare it to our approach assuming theoretical number of operations. + - Optimize construction of A regularization matrix. + - Compute BtB directly during single assembly pass, local 9x9 matricies as in A matrix. + - In contradiction to some literature (Hoschek) solution of the LS system is fast as long as the basis is local ( + this is true for B-splines). + - Extensions to fitting X and Y as well - general Surface + + """ diff --git a/src/bspline_plot.py b/src/bspline_plot.py index 6c843d8..2542444 100644 --- a/src/bspline_plot.py +++ b/src/bspline_plot.py @@ -22,7 +22,12 @@ def plot_curve_2d(curve, n_points=100, **kwargs): coords = [curve.eval(t) for t in t_coord] x_coord, y_coord = zip(*coords) - return plt.plot(x_coord, y_coord, **kwargs) + + img = plt.plot(x_coord, y_coord, **kwargs) + if kwargs.get('poles', False): + plot_curve_poles_2d(curve) + + return img def plot_curve_poles_2d(curve, **kwargs): @@ -70,7 +75,11 @@ def plot_surface_3d(surface, fig_ax, n_points=(100, 100), **kwargs): Z = Z.reshape(U.shape) # Plot the surface. - return fig_ax.plot_surface(X, Y, Z, **kwargs) + img = fig_ax.plot_surface(X, Y, Z, **kwargs) + if kwargs.get('poles', False): + plot_curve_poles_2d(surface) + return img + def plot_surface_poles_3d(surface, fig_ax, **kwargs): diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index 0c54d5b..0ff7016 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -39,13 +39,13 @@ def plot_approx_transformed_grid(self): self.plot_surf(z_surf) def plot_plane(self): - surf = bs_approx.plane_surface([ [0, 0, 0], [1,1,0], [0,0,1] ]) + surf = bs_approx.plane_surface([ [0.0, 0, 0], [1.0, 0, 0], [0.0, 0, 1] ], overhang=0.1) self.plot_surf(surf) def test_surface_approx(self): #self.plot_approx_grid() #self.plot_approx_transformed_grid() - self.plot_plane() + #self.plot_plane() pass @@ -63,5 +63,5 @@ def test_approx_2d(self): bs_plot.plot_curve_poles_2d(curve) - plt.plot(x_vec, y_vec, color='green') - plt.show() \ No newline at end of file + #plt.plot(x_vec, y_vec, color='green') + #plt.show() \ No newline at end of file diff --git a/tests/test_bspline.py b/tests/test_bspline.py index 9d05aa1..a766bfe 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -3,11 +3,18 @@ import numpy as np import math import bspline_plot as bs_plot -import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +import matplotlib.pyplot as plt class TestSplineBasis: + def test_find_knot_interval(self): + """ + test methods: + - make_equidistant + - find_knot_interval + """ eq_basis = bs.SplineBasis.make_equidistant(2, 100) assert eq_basis.find_knot_interval(0.0) == 0 assert eq_basis.find_knot_interval(0.001) == 0 @@ -27,8 +34,22 @@ def test_find_knot_interval(self): i_found = basis.find_knot_interval(x) assert i_found == interval - 2, "i_found: {} i: {} j: {} x: {} ".format(i_found, interval-2, j, x) - def plot_basis(self, degree): - eq_basis = bs.SplineBasis.make_equidistant(degree, 4) + + def test_packed_knots(self): + """ + Test: + - make_from_packed_knots + - pack_knots + :return: + """ + packed = [(-0.1, 3), (0,1), (1,1), (1.1, 3)] + basis = bs.SplineBasis.make_from_packed_knots(2, packed) + assert packed == basis.pack_knots() + + + + + def plot_basis(self, eq_basis): n_points = 401 x_coord = np.linspace(eq_basis.domain[0], eq_basis.domain[1], n_points) @@ -40,22 +61,11 @@ def plot_basis(self, degree): def test_eval(self): - #self.plot_basis(0) - #self.plot_basis(1) - #self.plot_basis(2) - #self.plot_basis(3) + #self.plot_basis(bs.SplineBasis.make_equidistant(0, 4)) knots = np.array([0, 0, 0, 0.1880192, 0.24545785, 0.51219762, 0.82239001, 1., 1. , 1.]) basis = bs.SplineBasis(2, knots) - n_points = 100 - x_coord = np.linspace(basis.domain[0], basis.domain[1], n_points) - - for i_base in range(basis.size): - y_coord = [ basis.eval(i_base, x) for x in x_coord ] - plt.plot(x_coord, y_coord) - - plt.show() - + # self.plot_basis(basis) eq_basis = bs.SplineBasis.make_equidistant(0, 2) assert eq_basis.eval(0, 0.0) == 1.0 @@ -75,6 +85,23 @@ def test_eval(self): assert eq_basis.eval(1, 0.125) == 0.5 assert eq_basis.eval(2, 1.0) == 0.0 + # check summation to one: + for deg in range(0, 10): + basis = bs.SplineBasis.make_equidistant(deg, 2) + for x in np.linspace(basis.domain[0], basis.domain[1], 10): + s = sum([ basis.eval(i, x) for i in range(basis.size) ]) + assert np.isclose(s, 1.0) + + def fn_supp(self): + basis = bs.SplineBasis.make_equidistant(2, 4) + for i in range(basis.size): + supp = basis.fn_supp(i) + for x in np.linspace(supp[0] - 0.1, supp[0], 10): + assert basis.eval(i, x) == 0.0 + for x in np.linspace(supp[0] + 0.001, supp[1] - 0.001, 10): + assert basis.eval(i, x) > 0.0 + for x in np.linspace(supp[1], supp[1] + 0.1): + assert basis.eval(i, x) == 0.0 def test_linear_poles(self): eq_basis = bs.SplineBasis.make_equidistant(2, 4) @@ -86,10 +113,8 @@ def test_linear_poles(self): x = np.dot(b_vals, poles) assert np.abs( x - t ) < 1e-15 - - def check_eval_vec(self, basis, i, t): - vec = basis.eval_base_vector(i, t) + vec = basis.eval_vector(i, t) for j in range(basis.degree + 1): assert vec[j] == basis.eval(i + j, t) @@ -100,7 +125,7 @@ def plot_basis_vec(self, basis): y_coords = np.zeros( (basis.size, x_coord.shape[0]) ) for i, x in enumerate(x_coord): idx = basis.find_knot_interval(x) - y_coords[idx : idx + basis.degree + 1, i] = basis.eval_base_vector(idx, x) + y_coords[idx : idx + basis.degree + 1, i] = basis.eval_vector(idx, x) for i_base in range(basis.size): plt.plot(x_coord, y_coords[i_base, :]) @@ -108,7 +133,7 @@ def plot_basis_vec(self, basis): plt.show() - def test_eval_base_vec(self): + def test_eval_vec(self): basis = bs.SplineBasis.make_equidistant(2, 4) # self.plot_basis_vec(basis) self.check_eval_vec(basis, 0, 0.1) @@ -117,7 +142,6 @@ def test_eval_base_vec(self): self.check_eval_vec(basis, 3, 0.8) self.check_eval_vec(basis, 3, 1.0) - basis = bs.SplineBasis.make_equidistant(3, 4) # self.plot_basis_vec(basis) self.check_eval_vec(basis, 0, 0.1) @@ -129,7 +153,7 @@ def test_eval_base_vec(self): def check_diff_vec(self, basis, i, t): - vec = basis.eval_diff_base_vector(i, t) + vec = basis.eval_diff_vector(i, t) for j in range(basis.degree + 1): assert np.abs(vec[j] - basis.eval_diff(i + j, t)) < 1e-15 @@ -140,7 +164,7 @@ def plot_basis_diff(self, basis): y_coords = np.zeros( (basis.size, x_coord.shape[0]) ) for i, x in enumerate(x_coord): idx = basis.find_knot_interval(x) - y_coords[idx : idx + basis.degree + 1, i] = basis.eval_diff_base_vector(idx, x) + y_coords[idx : idx + basis.degree + 1, i] = basis.eval_diff_vector(idx, x) for i_base in range(basis.size): plt.plot(x_coord, y_coords[i_base, :]) @@ -166,6 +190,7 @@ def test_eval_diff_base_vec(self): self.check_diff_vec(basis, 3, 1.0) + class TestCurve: def plot_4p(self): @@ -174,15 +199,24 @@ def plot_4p(self): basis = bs.SplineBasis.make_equidistant(degree, 2) curve = bs.Curve(basis, poles) - bs_plot.plot_curve_2d(curve) - bs_plot.plot_curve_poles_2d(curve) + bs_plot.plot_curve_2d(curve, poles=True) + b00, b11 = curve.aabb() + b01 = [b00[0], b11[1]] + b10 = [b11[0], b00[1]] + bb = np.array([b00, b10, b11, b01, b00]) + + plt.plot( bb[:, 0], bb[:, 1], color='green') plt.show() def test_evaluate(self): # self.plot_4p() + # TODO: make numerical tests with explicitely computed values + # TODO: test rational curves, e.g. circle + pass - # TODO: test rational curves, e.g. circle + + @@ -204,8 +238,7 @@ def plot_extrude(self): u_basis = bs.SplineBasis.make_equidistant(2, 1) v_basis = bs.SplineBasis.make_equidistant(2, 2) surface_extrude = bs.Surface( (u_basis, v_basis), poles) - bs_plot.plot_surface_3d(surface_extrude, ax) - bs_plot.plot_surface_poles_3d(surface_extrude, ax) + bs_plot.plot_surface_3d(surface_extrude, ax, poles = True) plt.show() def plot_function(self): @@ -226,14 +259,10 @@ def function(x): plt.show() def test_evaluate(self): - from mpl_toolkits.mplot3d import Axes3D - import matplotlib.pyplot as plt - # self.plot_extrude() # self.plot_function() - - # TODO: test rational surfaces, e.g. sphere + pass @@ -282,8 +311,7 @@ def function(x): def make_point_grid(self): nu, nv = 5,6 grid = bs.make_function_grid(TestPointGrid.function, 5, 6).reshape(nu*nv, 3) - surf = bs.GridSurface() - surf.init_from_seq(grid.T) + surf = bs.GridSurface(grid) return surf def check_surface(self, surf, xy_mat, xy_shift, z_mat): @@ -308,9 +336,12 @@ def check_surface(self, surf, xy_mat, xy_shift, z_mat): uvz = np.concatenate( (UV.T, Z_grid[:, None]), axis = 1) for u, v, z_approx in uvz: - z_func = z_mat[0]*self.function([u,v]) + z_mat[1] + z_func = z_mat[0]*TestPointGrid.function([u,v]) + z_mat[1] eps = max(eps, math.fabs( z_approx - z_func)) - assert math.fabs( z_approx - z_func) < tol + assert np.isclose(z_approx, z_func, atol = tol) + + x, y, z = surf.eval_array(np.array([[u,v]]))[0] + assert np.isclose(z_approx, z, atol=tol) print("Max norm: ", eps, "Tol: ", tol) def test_grid_surface(self): From 6fef8208f7b106d3fe40b952b481b5cbef97da19 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 20 Sep 2017 14:15:53 +0200 Subject: [PATCH 26/36] Fix bspline and its tests. --- src/bspline.py | 11 +++++--- src/bspline_plot.py | 45 ++++++++++++++++++++++++++++++-- tests/test_bs_approx.py | 9 +++++-- tests/test_bspline.py | 57 +++++++++++++++++++++++++++++------------ 4 files changed, 98 insertions(+), 24 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index 4219ad8..e6a5abc 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -902,8 +902,8 @@ def transform(self, xy_mat, z_mat): self.quad = self.z_surface.uv_to_xy( np.array([[0, 1], [0, 0], [1, 0], [1, 1]]) ) # transform z_scale - self.z_scale = 1.0 - self.z_shift = 0.0 + self.z_scale *= z_mat[0] + self.z_shift = z_mat[0]*self.z_shift + z_mat[1] def xy_to_uv(self, xy_points): @@ -923,7 +923,10 @@ def uv_to_xy(self, uv_points): def eval_array(self, uv_points): - return self.z_surface.eval_array(uv_points) + xyz = self.z_surface.eval_array(uv_points) + xyz[:, 2] *= self.z_scale + xyz[:, 2] += self.z_shift + return xyz def z_eval_xy_array(self, xy_points): @@ -957,7 +960,7 @@ def z_eval_array(self, uv_points): u_loc = np.array([1 - uv_loc[0], uv_loc[0]]) v_loc = np.array([1 - uv_loc[1], uv_loc[1]]) Z_mat = self.grid_uvz[iu: (iu + 2), iv: (iv + 2), 2] - result[i] = self.z_scale*(v_loc.dot(Z_mat).dot(u_loc)) + self.z_shift + result[i] = self.z_scale*(u_loc.dot(Z_mat).dot(v_loc)) + self.z_shift return result diff --git a/src/bspline_plot.py b/src/bspline_plot.py index 2542444..e24b7cf 100644 --- a/src/bspline_plot.py +++ b/src/bspline_plot.py @@ -23,8 +23,9 @@ def plot_curve_2d(curve, n_points=100, **kwargs): coords = [curve.eval(t) for t in t_coord] x_coord, y_coord = zip(*coords) + poles = kwargs.pop('poles', False) img = plt.plot(x_coord, y_coord, **kwargs) - if kwargs.get('poles', False): + if poles: plot_curve_poles_2d(curve) return img @@ -41,6 +42,7 @@ def plot_curve_poles_2d(curve, **kwargs): return plt.plot(x_poles, y_poles, 'bo', color='red', **kwargs) + def plot_surface_3d(surface, fig_ax, n_points=(100, 100), **kwargs): """ Plot a surface in 3d. @@ -75,13 +77,52 @@ def plot_surface_3d(surface, fig_ax, n_points=(100, 100), **kwargs): Z = Z.reshape(U.shape) # Plot the surface. + poles = kwargs.pop('poles', False) + img = fig_ax.plot_surface(X, Y, Z, **kwargs) - if kwargs.get('poles', False): + if poles: plot_curve_poles_2d(surface) return img +def plot_grid_surface_3d(surface, fig_ax, n_points=(100, 100), **kwargs): + """ + Plot a surface in 3d. + Usage: + from mpl_toolkits.mplot3d import Axes3D + import matplotlib.pyplot as plt + fig = plt.figure() + ax = fig.gca(projection='3d') + plot_surface_3d(surface, ax) + plt.show() + + :param surface: Parametric surface in 3d. + :param fig_ax: Axes object: + :param n_points: (nu, nv), nu*nv - number of evaluation point + :param kwargs: surface_plot additional options + :return: The plot object. + """ + + u_coord = np.linspace(0, 1.0, n_points[0]) + v_coord = np.linspace(0, 1.0, n_points[1]) + + U, V = np.meshgrid(u_coord, v_coord) + points = np.stack( [U.ravel(), V.ravel()], axis = 1 ) + + xyz = surface.eval_array(points) + X, Y, Z = xyz.T + + X = X.reshape(U.shape) + Y = Y.reshape(U.shape) + Z = Z.reshape(U.shape) + + #Z = surface.z_eval_array(points).reshape(U.shape) + # Plot the surface. + img = fig_ax.plot_surface(U, V, Z, **kwargs) + return img + + def plot_surface_poles_3d(surface, fig_ax, **kwargs): """ Plot poles of the B-spline curve. diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index 0ff7016..eab5418 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -53,7 +53,7 @@ def test_surface_approx(self): class TestCurveApprox: - def test_approx_2d(self): + def plot_approx_2d(self): x_vec = np.linspace(1.1, 3.0, 100) y_vec = np.array([ np.sin(10*x) for x in x_vec ]) points = np.stack( (x_vec, y_vec), axis=1) @@ -62,6 +62,11 @@ def test_approx_2d(self): bs_plot.plot_curve_2d(curve, 1000) bs_plot.plot_curve_poles_2d(curve) + plt.show() #plt.plot(x_vec, y_vec, color='green') - #plt.show() \ No newline at end of file + #plt.show() + + def test_approx_2d(self): + # self.plot_approx_2d() + pass \ No newline at end of file diff --git a/tests/test_bspline.py b/tests/test_bspline.py index a766bfe..e18c17d 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -314,6 +314,25 @@ def make_point_grid(self): surf = bs.GridSurface(grid) return surf + + def plot_check_surface(self, XYZ_grid_eval, XYZ_surf_eval, XYZ_func_eval): + fig = plt.figure() + ax = fig.gca(projection='3d') + ax.plot_surface(XYZ_grid_eval[:, :, 0], XYZ_grid_eval[:, :, 1], XYZ_grid_eval[:, :, 2], color='blue') + ax.plot_surface(XYZ_surf_eval[:, :, 0], XYZ_surf_eval[:, :, 1], XYZ_surf_eval[:, :, 2], color='red') + ax.plot_surface(XYZ_func_eval[:, :, 0], XYZ_func_eval[:, :, 1], XYZ_func_eval[:, :, 2], color='green') + plt.show() + + def grid_cmp(self, a, b, tol): + a_z = a[:, :, 2].ravel() + b_z = b[:, :, 2].ravel() + eps = 0.0 + for i, (za, zb) in enumerate(zip(a_z, b_z)): + diff = np.abs( za - zb) + eps = max(eps, diff) + assert diff < tol, " |a({}) - b({})| > tol({}), idx: {}".format(za, zb, tol, i) + print("Max norm: ", eps, "Tol: ", tol) + def check_surface(self, surf, xy_mat, xy_shift, z_mat): """ TODO: Make this a general function - evaluate a surface on a grid, use it also in other tests @@ -324,25 +343,27 @@ def check_surface(self, surf, xy_mat, xy_shift, z_mat): # surface on unit square U = np.linspace(0.0, 1.0, nu) V = np.linspace(0.0, 1.0, nv) - U_grid, V_grid = np.meshgrid(U,V) + V_grid, U_grid = np.meshgrid(V,U) + + UV = np.stack( [U_grid.ravel(), V_grid.ravel()], axis = 1 ) + XY = xy_mat.dot(UV.T).T + xy_shift + Z = surf.z_eval_xy_array(XY) + XYZ_grid_eval = np.concatenate( (XY, Z[:, None]) , axis = 1).reshape(nu, nv, 3) + + XYZ_surf_eval = surf.eval_array(UV).reshape(nu, nv, 3) + + z_func_eval = np.array([ z_mat[0]*TestPointGrid.function([u,v]) + z_mat[1] for u, v in UV ]) + XYZ_func_eval = np.concatenate( (XY, z_func_eval[:, None]), axis =1 ).reshape(nu, nv, 3) + + #self.plot_check_surface(XYZ_grid_eval, XYZ_surf_eval, XYZ_func_eval) - UV = np.vstack([U_grid.ravel(), V_grid.ravel()]) - XY = xy_mat.dot(UV).T + xy_shift - Z_grid = surf.z_eval_xy_array(XY) eps = 0.0 hx = 1.0 / surf.shape[0] hy = 1.0 / surf.shape[1] - tol = 0.5* ( hx*hx + 2*hx*hy + hy*hy) + tol = 2.0 * 0.5* ( hx*hx + 2*hx*hy + hy*hy) - uvz = np.concatenate( (UV.T, Z_grid[:, None]), axis = 1) - for u, v, z_approx in uvz: - z_func = z_mat[0]*TestPointGrid.function([u,v]) + z_mat[1] - eps = max(eps, math.fabs( z_approx - z_func)) - assert np.isclose(z_approx, z_func, atol = tol) - - x, y, z = surf.eval_array(np.array([[u,v]]))[0] - assert np.isclose(z_approx, z, atol=tol) - print("Max norm: ", eps, "Tol: ", tol) + self.grid_cmp(XYZ_func_eval, XYZ_grid_eval, tol) + self.grid_cmp(XYZ_func_eval, XYZ_surf_eval, tol) def test_grid_surface(self): xy_mat = np.array([ [1.0, 0.0], [0.0, 1.0] ]) @@ -350,7 +371,11 @@ def test_grid_surface(self): z_shift = np.array([1.0, 0.0]) surface = self.make_point_grid() - self.check_surface(surface, xy_mat, xy_shift, z_shift) + # fig = plt.figure() + # ax = fig.gca(projection='3d') + # bs_plot.plot_grid_surface_3d(surface, ax) + # plt.show() + # self.check_surface(surface, xy_mat, xy_shift, z_shift) # transformed surface xy_mat = np.array([ [3.0, -3.0], [2.0, 2.0] ]) / math.sqrt(2) @@ -358,7 +383,7 @@ def test_grid_surface(self): z_shift = np.array([1.0, 1.3]) surface = self.make_point_grid() - surface.z_surface.transform(np.concatenate((xy_mat, xy_shift.T), axis=1), z_shift) + surface.transform(np.concatenate((xy_mat, xy_shift.T), axis=1), z_shift) self.check_surface(surface, xy_mat, xy_shift, z_shift) From 45de1cebb51447061deb41d0f52077905219383a Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 20 Sep 2017 20:35:57 +0200 Subject: [PATCH 27/36] Add check method to SplineBase - add check method for rounding values cloase to boundary of the domain - few more fixes in GridSurface --- src/bspline.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index e6a5abc..ceb5259 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -174,6 +174,27 @@ def pack_knots(self): return packed_knots + def check(self, t, rtol = 1e-10): + """ + Check that 't' is inside basis domain. Fix small perturbations. + :param t: + :return: + """ + if self.domain[0] <= t <= self.domain[1]: + return t + else: + tol = (np.abs(t) + 1e-4)*rtol + assert self.domain[0] - tol <= t <= self.domain[1] + tol, \ + "Evaluate spline, t={}, out of domain: {}.".format(t, self.domain) + if t < self.domain[0]: + return self.domain[0] + else: + return self.domain[1] + + + + self.domain[0] + def find_knot_interval(self, t): """ Find the first non-empty knot interval containing the value 't'. @@ -189,6 +210,7 @@ def find_knot_interval(self, t): return min(idx, self.n_intervals - 1) # deals with t == self.knots[-1] + def _basis(self, deg, idx, t): """ Recursive evaluation of basis function of given degree and index. @@ -514,7 +536,8 @@ def eval(self, u, v): - use basis.eval_vector - test evaluation for rational curves """ - + u = self.u_basis.check(u) + v = self.v_basis.check(v) iu = self.u_basis.find_knot_interval(u) iv = self.v_basis.find_knot_interval(v) du = self.u_basis.degree + 1 @@ -797,18 +820,13 @@ def __init__(self, point_seq, tolerance = 1e-6): self.z_scale = 1.0 self.z_shift = 0.0 - # self._grid_z = point_seq[2, :].reshape(self.nv, self.nu) - # grid of original Z values, format as matrix - - self._uv_step = (1.0 / float(self.shape[0] - 1), 1.0 / float(self.shape[1] - 1)) # Grid step in u, v direction respectively. - self._make_z_surface( point_seq) - - self.points_xyz = point_seq - # Original sequance of XYZ points + self.grid_uvz = None + # numpy array nu x nv x 3 with original XYZ values transformed to unit square. + self._make_z_surface( point_seq) self._check_map() @@ -881,7 +899,7 @@ def _make_z_surface(self, points): def _check_map(self): # check that xy_to_uv works fine uv_quad = self.xy_to_uv(self.quad) - print( uv_quad ) + #print( "Check quad: ", uv_quad ) assert np.allclose( uv_quad, np.array([[0, 1], [0, 0], [1, 0], [1, 1]]) ) From 17daabac538c207012961f1746005325bc3c878d Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 20 Sep 2017 20:37:47 +0200 Subject: [PATCH 28/36] Fixes and new tests for approximation - surface approximation modify size of approximation in order to have well posed problem (more points then unknowns) - fix several bugs - add numerical tests --- src/bspline_approx.py | 91 ++++++++++++++++++++-------- tests/test_bs_approx.py | 127 +++++++++++++++++++++++++++++++++++----- tests/test_bspline.py | 5 +- 3 files changed, 180 insertions(+), 43 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 2b01a8c..479abf4 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -144,11 +144,11 @@ class _SurfaceApprox: - def __init__(self, grid_surface, nuv, **kwargs): + def __init__(self, grid_surface, nuv=None, **kwargs): + self.degree = np.array((2, 2)) self.grid_surf = grid_surface - self.u_basis = bs.SplineBasis.make_equidistant(2, nuv[0]) - self.v_basis = bs.SplineBasis.make_equidistant(2, nuv[1]) - self.regularization_weight = kwargs.get('reg_weight', 1.0) + self.nuv = nuv + self.regularization_weight = kwargs.get('reg_weight', 0.001) def get_approximation(self): if not hasattr(self, 'z_surf'): @@ -172,28 +172,53 @@ def approx_chol(self): print('Transforming points to parametric space ...') start_time = time.time() - points = self.grid_surf.points_xyz - points_uv = self.grid_surf.xy_to_uv( points[:, 0:2] ) + points_uvz = self.grid_surf.grid_uvz.reshape(-1, 3) + points_uv = points_uvz[:, 0:2] # remove points far from unit square eps = 1.0e-15 cut_min = np.array( [ -eps, -eps ]) cut_max = np.array( [ 1+eps, 1+eps ]) - in_idx = np.all(np.logical_and(cut_min < points_uv, points_uv <= cut_max), axis=1) points_uv = points_uv[in_idx] - points_z = points[in_idx, 2][:,None] + self.points_z = points_uvz[in_idx, 2][:,None] + print("Number of points out of the grid domain: {}".format(len(points_uv) - np.sum(in_idx))) # snap to unit square points_uv = np.maximum(points_uv, np.array([0.0, 0.0])) - points_uv = np.minimum(points_uv, np.array([1.0, 1.0])) - + self.points_uv = np.minimum(points_uv, np.array([1.0, 1.0])) + + # determine number of knots + assert len(self.points_uv) == len(self.points_z) + n_points = len(self.points_uv) + + grid_shape = np.asarray(self.grid_surf.shape, dtype=int) + if self.nuv == None: + nuv = np.floor_divide( grid_shape / 3 ) + else: + nuv = np.floor(np.array(self.nuv)) + nuv = np.minimum( nuv, grid_shape - self.degree - 2) + size_u, size_v = nuv + self.degree + + # try to make number of unknowns less then number of remaining points + # +1 to improve determination + if (size_u + 1) * (size_v + 1) > n_points: + sv = np.floor(np.sqrt( n_points * size_v / size_u )) + su = np.floor(sv * size_u / size_v) + nuv = np.array( [su, sv] ) - self.degree + nuv = nuv.astype(int) + if nuv[0] < 1 or nuv[1] < 1: + raise Exception("Two few points, {}, to make approximation, degree: {}".format(n_points, self.degree)) + + print("Using {} x {} B-spline approximation.".format(nuv[0], nuv[1])) + self.u_basis = bs.SplineBasis.make_equidistant(2, nuv[0]) + self.v_basis = bs.SplineBasis.make_equidistant(2, nuv[1]) - self.grid_uvz = np.concatenate((points_uv, points_z), axis=1) end_time = time.time() print('Computed in {0} seconds.'.format(end_time - start_time)) + # Own computation of approximation print('Creating B matrix ...') start_time = time.time() @@ -215,35 +240,45 @@ def approx_chol(self): print('Computed in {0} seconds.'.format(end_time - start_time)) + print('Computing A and B svds approximation ...') + start_time = time.time() + bb_norm = scipy.sparse.linalg.svds(bb_mat, k=1, ncv=10, tol=1e-4, which='LM', v0=None, maxiter=300, return_singular_vectors=False) a_norm = scipy.sparse.linalg.svds(a_mat, k=1, ncv=10, tol=1e-4, which='LM', v0=None, maxiter=300, return_singular_vectors=False) c_mat = bb_mat + self.regularization_weight * (bb_norm[0] / a_norm[0]) * a_mat - g_vec = self.grid_uvz[:, 2] + g_vec = self.points_z b_vec = b_mat.transpose() * g_vec + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) print('Computing Z coordinates ...') start_time = time.time() z_vec = scipy.sparse.linalg.spsolve(c_mat, b_vec) + assert not np.isnan(np.sum(z_vec)), "Singular matrix for approximation." + print(type(z_vec)) end_time = time.time() print('Computed in {0} seconds.'.format(end_time - start_time)) - # print('Computing differences ...') - # start_time = time.time() - # diff = (numpy.matrix(b_mat * z_vec).transpose() - g_vec).tolist() - # diff = [item[0] for item in diff] - # end_time = time.time() - # print('Computed in {0} seconds.'.format(end_time - start_time)) + print('Computing differences ...') + start_time = time.time() + diff = np.abs(b_mat * z_vec - g_vec) + max_diff = np.max(diff) + print("Approximation error (max norm): {}".format(max_diff) ) + end_time = time.time() + print('Computed in {0} seconds.'.format(end_time - start_time)) # Construct Z-Surface - poles_z = z_vec.reshape(self.u_basis.size, self.v_basis.size, 1) - surface_z = bs.Surface((self.u_basis, self.v_basis), poles_z) - z_surf = bs.Z_Surface(self.grid_surf.quad, surface_z) + poles_z = z_vec.reshape(self.v_basis.size, self.u_basis.size).T + poles_z *= self.grid_surf.z_scale + poles_z += self.grid_surf.z_shift + surface_z = bs.Surface((self.u_basis, self.v_basis), poles_z[:,:,None]) + z_surf = bs.Z_Surface(self.grid_surf.quad[0:3], surface_z) return z_surf @@ -261,7 +296,7 @@ def build_ls_matrix(self): """ u_n_basf = self.u_basis.size v_n_basf = self.v_basis.size - n_points = self.grid_uvz.shape[0] + n_points = self.points_uv.shape[0] n_uv_loc_nz = (self.u_basis.degree + 1) * (self.v_basis.degree + 1) row = np.zeros(n_points * n_uv_loc_nz) @@ -273,16 +308,19 @@ def build_ls_matrix(self): interval = np.empty((n_points, 2)) for idx in range(0, n_points): - u, v = self.grid_uvz[idx, 0:2] + u, v = self.points_uv[idx, 0:2] iu = self.u_basis.find_knot_interval(u) - iv = self.u_basis.find_knot_interval(v) + iv = self.v_basis.find_knot_interval(v) u_base_vec = self.u_basis.eval_base_vector(iu, u) - v_base_vec = self.u_basis.eval_base_vector(iv, v) + v_base_vec = self.v_basis.eval_base_vector(iv, v) # Hard-coded Kronecker product (problem based) for n in range(0, 3): data[nnz_b + 3 * n:nnz_b + 3 * (n + 1)] = v_base_vec[n] * u_base_vec for m in range(0, 3): - col[nnz_b + (3 * n) + m] = (iv + n) * u_n_basf + iu + m + col_item = (iv + n) * u_n_basf + iu + m + print(idx, iu, iv, n, m, col_item) + + col[nnz_b + (3 * n) + m] = col_item row[nnz_b:nnz_b + 9] = idx nnz_b += 9 @@ -308,6 +346,7 @@ def _basis_in_q_points(self, basis): for j in range(nq_points): up = us + uil * self._q_points[j] q_point[n] = up + # TODO: no need to find interval, idx = i idx = basis.find_knot_interval(up) u_base_vec = basis.eval_base_vector(idx, up) u_base_vec_diff = basis.eval_diff_base_vector(idx, up) diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index eab5418..536c1b2 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -11,6 +11,68 @@ def function_sin_cos(x): return math.sin(x[0] * 4) * math.cos(x[1] * 4) +def gen_uv_grid(nu, nv): + # surface on unit square + U = np.linspace(0.0, 1.0, nu) + V = np.linspace(0.0, 1.0, nv) + V_grid, U_grid = np.meshgrid(V, U) + + return np.stack([U_grid.ravel(), V_grid.ravel()], axis=1) + +def eval_func_on_grid(func, xy_mat, xy_shift, z_mat, shape=(50, 50)): + nu, nv = shape + UV = gen_uv_grid(nu, nv) + XY = xy_mat.dot(UV.T).T + xy_shift + z_func_eval = np.array([z_mat[0] * func([u, v]) + z_mat[1] for u, v in UV]) + return np.concatenate((XY, z_func_eval[:, None]), axis=1).reshape(nu, nv, 3) + + +def eval_z_surface_on_grid(surface, xy_mat, xy_shift, shape=(50, 50)): + nu, nv = shape + UV = gen_uv_grid(nu, nv) + XY = xy_mat.dot(UV.T).T + xy_shift + Z = surface.z_eval_xy_array(XY) + return np.concatenate((XY, Z[:, None]), axis=1).reshape(nu, nv, 3) + + +def eval_surface_on_grid(surface, shape=(50, 50)): + nu, nv = shape + UV = gen_uv_grid(nu, nv) + return surface.eval_array(UV).reshape(nu, nv, 3) + +def plot_cmp(a_grid, b_grid): + fig = plt.figure() + ax = fig.gca(projection='3d') + ax.plot_surface(a_grid[:, :, 0], a_grid[:, :, 1], a_grid[:, :, 2], color='green') + ax.plot_surface(b_grid[:, :, 0], b_grid[:, :, 1], b_grid[:, :, 2], color='red') + plt.show() + + diff = b_grid - a_grid + fig = plt.figure() + ax = fig.gca(projection='3d') + ax.plot_surface(a_grid[:, :, 0], a_grid[:, :, 1], diff[:, :, 0], color='red') + ax.plot_surface(a_grid[:, :, 0], a_grid[:, :, 1], diff[:, :, 1], color='green') + ax.plot_surface(a_grid[:, :, 0], a_grid[:, :, 1], diff[:, :, 2], color='blue') + plt.show() + +def grid_cmp( a, b, tol): + a_z = a[:, :, 2].ravel() + b_z = b[:, :, 2].ravel() + eps = 0.0 + n_err=0 + for i, (za, zb) in enumerate(zip(a_z, b_z)): + diff = np.abs( za - zb) + eps = max(eps, diff) + if diff > tol: + n_err +=1 + if n_err < 10: + print(" {} =|a({}) - b({})| > tol({}), idx: {}".format(diff, za, zb, tol, i) ) + elif n_err == 10: + print("... skipping") + print("Max norm: ", eps, "Tol: ", tol) + if eps > tol: + plot_cmp(a, b) + class TestSurfaceApprox: # todo: numerical test @@ -21,31 +83,64 @@ def plot_surf(self, surf): plt.show() - def plot_approx_grid(self): - points = bs.make_function_grid(function_sin_cos, 20, 30) - gs = bs.GridSurface() - gs.init_from_seq(points.reshape(-1, 3).T) - z_surf = bs_approx.surface_from_grid(gs, (3,4) ) - self.plot_surf(z_surf) + def test_approx_func(self): + xy_mat = np.array( [ [1.0, -1.0, 10 ], [1.0, 1.0, 20 ]]) # rotate left pi/4 and blow up 1.44 + z_mat = np.array( [2.0, -10] ) + + xyz_func = eval_func_on_grid(function_sin_cos, xy_mat[:2,:2], xy_mat[:, 2], z_mat) + + print("Compare: Func - GridSurface.transform") + points = bs.make_function_grid(function_sin_cos, 40, 30) + gs = bs.GridSurface(points.reshape(-1, 3)) + gs.transform(xy_mat, z_mat) + xyz_grid = eval_z_surface_on_grid(gs, xy_mat[:2,:2], xy_mat[:, 2]) + #grid_cmp(xyz_func, xyz_grid, 0.02) + + print("Compare: Func - GridSurface.transform.z_surf") + xyz_grid = eval_z_surface_on_grid(gs.z_surface, xy_mat[:2, :2], xy_mat[:, 2]) + xyz_grid[:,:,2] *= z_mat[0] + xyz_grid[:, :, 2] += z_mat[1] + #grid_cmp(xyz_func, xyz_grid, 0.02) + + print("Compare: Func - GridSurface.transform.Z_surf_approx") + z_surf = bs_approx.surface_from_grid(gs, (7, 5)) + #z_surf.transform(xy_mat, z_mat) + xyz_grid = eval_z_surface_on_grid(z_surf, xy_mat[:2,:2], xy_mat[:, 2]) + grid_cmp(xyz_func, xyz_grid, 0.02) + + print("Compare: Func - GridSurface.transform.Z_surf_approx.full_surface") + xyz_grid = eval_surface_on_grid(z_surf.make_full_surface()) + #grid_cmp(xyz_func, xyz_grid, 0.01) + + + + + + + #points = bs.make_function_grid(function_sin_cos, 20, 30) + #points = np.dot(points, mat.T) + np.array([10, 20, -5.0]) + #gs = bs.GridSurface(points.reshape(-1, 3)) + #z_surf = bs_approx.surface_from_grid(gs, (3,4) ) + #self.plot_surf(z_surf) + + + + + + def plot_approx_transformed_grid(self): - points = bs.make_function_grid(function_sin_cos, 20, 30) - mat = np.array( [ [2.0, 1.0, 0.0 ], [1.0, 2.0, 0.0 ], [0.0, 0.0, 0.5 ]]) - points = np.dot(points, mat.T) + np.array([10, 20, -5.0]) - gs = bs.GridSurface() - gs.init_from_seq(points.reshape(-1, 3).T) - z_surf = bs_approx.surface_from_grid(gs, (3,4) ) - self.plot_surf(z_surf) + pass def plot_plane(self): surf = bs_approx.plane_surface([ [0.0, 0, 0], [1.0, 0, 0], [0.0, 0, 1] ], overhang=0.1) self.plot_surf(surf) def test_surface_approx(self): - #self.plot_approx_grid() - #self.plot_approx_transformed_grid() - #self.plot_plane() + # self.plot_approx_grid() + # self.plot_approx_transformed_grid() + # self.plot_plane() pass diff --git a/tests/test_bspline.py b/tests/test_bspline.py index e18c17d..b488d73 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -360,7 +360,7 @@ def check_surface(self, surf, xy_mat, xy_shift, z_mat): eps = 0.0 hx = 1.0 / surf.shape[0] hy = 1.0 / surf.shape[1] - tol = 2.0 * 0.5* ( hx*hx + 2*hx*hy + hy*hy) + tol = 0.5* ( hx*hx + 2*hx*hy + hy*hy) self.grid_cmp(XYZ_func_eval, XYZ_grid_eval, tol) self.grid_cmp(XYZ_func_eval, XYZ_surf_eval, tol) @@ -381,9 +381,12 @@ def test_grid_surface(self): xy_mat = np.array([ [3.0, -3.0], [2.0, 2.0] ]) / math.sqrt(2) xy_shift = np.array([[-2.0, 5.0 ]]) z_shift = np.array([1.0, 1.3]) + new_quad = np.array([ [0, 1.0], [0,0], [1, 0], [1, 1]]) + new_quad = new_quad.dot(xy_mat[:2,:2].T) + xy_shift surface = self.make_point_grid() surface.transform(np.concatenate((xy_mat, xy_shift.T), axis=1), z_shift) + assert np.all(surface.quad == new_quad), "surf: {} ref: {}".format(surface.quad, new_quad) self.check_surface(surface, xy_mat, xy_shift, z_shift) From 51d907b0f3bc384b6a2213f25972dee4f03d434e Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 20 Sep 2017 21:50:29 +0200 Subject: [PATCH 29/36] Fix computing differences --- src/bspline_approx.py | 10 ++++------ tests/test_bs_approx.py | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 479abf4..7b60048 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -234,7 +234,7 @@ def approx_chol(self): print('Computing B^T B matrix ...') start_time = time.time() - bb_mat = b_mat.transpose() * b_mat + bb_mat = b_mat.transpose().dot(b_mat) end_time = time.time() print('Computed in {0} seconds.'.format(end_time - start_time)) @@ -249,8 +249,8 @@ def approx_chol(self): maxiter=300, return_singular_vectors=False) c_mat = bb_mat + self.regularization_weight * (bb_norm[0] / a_norm[0]) * a_mat - g_vec = self.points_z - b_vec = b_mat.transpose() * g_vec + g_vec = self.points_z[:, 0] + b_vec = b_mat.transpose().dot( g_vec ) end_time = time.time() print('Computed in {0} seconds.'.format(end_time - start_time)) @@ -267,7 +267,7 @@ def approx_chol(self): print('Computing differences ...') start_time = time.time() - diff = np.abs(b_mat * z_vec - g_vec) + diff = b_mat.dot(z_vec) - g_vec max_diff = np.max(diff) print("Approximation error (max norm): {}".format(max_diff) ) end_time = time.time() @@ -318,8 +318,6 @@ def build_ls_matrix(self): data[nnz_b + 3 * n:nnz_b + 3 * (n + 1)] = v_base_vec[n] * u_base_vec for m in range(0, 3): col_item = (iv + n) * u_n_basf + iu + m - print(idx, iu, iv, n, m, col_item) - col[nnz_b + (3 * n) + m] = col_item row[nnz_b:nnz_b + 9] = idx nnz_b += 9 diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index 536c1b2..42340d0 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -90,27 +90,27 @@ def test_approx_func(self): xyz_func = eval_func_on_grid(function_sin_cos, xy_mat[:2,:2], xy_mat[:, 2], z_mat) print("Compare: Func - GridSurface.transform") - points = bs.make_function_grid(function_sin_cos, 40, 30) + points = bs.make_function_grid(function_sin_cos, 30, 40) gs = bs.GridSurface(points.reshape(-1, 3)) gs.transform(xy_mat, z_mat) xyz_grid = eval_z_surface_on_grid(gs, xy_mat[:2,:2], xy_mat[:, 2]) - #grid_cmp(xyz_func, xyz_grid, 0.02) + grid_cmp(xyz_func, xyz_grid, 0.02) print("Compare: Func - GridSurface.transform.z_surf") xyz_grid = eval_z_surface_on_grid(gs.z_surface, xy_mat[:2, :2], xy_mat[:, 2]) xyz_grid[:,:,2] *= z_mat[0] xyz_grid[:, :, 2] += z_mat[1] - #grid_cmp(xyz_func, xyz_grid, 0.02) + grid_cmp(xyz_func, xyz_grid, 0.02) print("Compare: Func - GridSurface.transform.Z_surf_approx") - z_surf = bs_approx.surface_from_grid(gs, (7, 5)) + z_surf = bs_approx.surface_from_grid(gs, (16, 24)) #z_surf.transform(xy_mat, z_mat) xyz_grid = eval_z_surface_on_grid(z_surf, xy_mat[:2,:2], xy_mat[:, 2]) grid_cmp(xyz_func, xyz_grid, 0.02) print("Compare: Func - GridSurface.transform.Z_surf_approx.full_surface") xyz_grid = eval_surface_on_grid(z_surf.make_full_surface()) - #grid_cmp(xyz_func, xyz_grid, 0.01) + grid_cmp(xyz_func, xyz_grid, 0.01) From a776944035972816297de3b5d7b41885d1fe3f28 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 20 Sep 2017 22:31:35 +0200 Subject: [PATCH 30/36] Add brep writer --- src/brep_writer.py | 1062 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1062 insertions(+) create mode 100644 src/brep_writer.py diff --git a/src/brep_writer.py b/src/brep_writer.py new file mode 100644 index 0000000..1edadaa --- /dev/null +++ b/src/brep_writer.py @@ -0,0 +1,1062 @@ +import enum +import itertools +import numpy as np +import bspline as bs + + +''' +TODO: +- For Solid make auto conversion from Faces similar to Face from Edges +- For Solid make test that Shells are closed. +- Implement closed test for shells (similar to wire) +- Improve test of slosed (Wire, Shell) to check also orientation of (Edges, Faces). ?? May be both if holes are allowed. +- Rename attributes and methods to more_words_are_separated_by_underscore. +- rename _writeformat to _brep_output +- remove (back) groups parameter from _brepo_output, make checks of IDs at main level (only in DEBUG mode) +- document public methods +''' + + +class ParamError(Exception): + pass + +def check_matrix(mat, shape, values, idx=[]): + ''' + Check shape and type of scalar, vector or matrix. + :param mat: Scalar, vector, or vector of vectors (i.e. matrix). Vector may be list or other iterable. + :param shape: List of dimensions: [] for scalar, [ n ] for vector, [n_rows, n_cols] for matrix. + If a value in this list is None, the dimension can be arbitrary. The shape list is set fo actual dimensions + of the matrix. + :param values: Type or tuple of allowed types of elements of the matrix. E.g. ( int, float ) + :param idx: Internal. Used to pass actual index in the matrix for possible error messages. + :return: + ''' + try: + + if len(shape) == 0: + if not isinstance(mat, values): + raise ParamError("Element at index {} of type {}, expected instance of {}.".format(idx, type(mat), values)) + else: + + if shape[0] is None: + shape[0] = len(mat) + l=None + if not hasattr(mat, '__len__'): + l=0 + elif len(mat) != shape[0]: + l=len(mat) + if not l is None: + raise ParamError("Wrong len {} of element {}, should be {}.".format(l, idx, shape[0])) + for i, item in enumerate(mat): + sub_shape = shape[1:] + check_matrix(item, sub_shape, values, idx = [i] + idx) + shape[1:] = sub_shape + return shape + except ParamError: + raise + except Exception as e: + raise ParamError(e) + + +class Location: + """ + Location defines an affine transformation in 3D space. Corresponds to the in the BREP file. + BREP format allows to use different transformations for individual shapes. + Location are numberd from 1. Zero index means identity location. + """ + def __init__(self, matrix=None): + """ + Constructor for elementary afine transformation. + :param matrix: Transformation matrix 3x4. First three columns forms the linear transformation matrix. + Last column is the translation vector. + matrix==None means identity location (ID=0). + """ + if matrix is None: + self.matrix = None + self.id = 0 + return + + # checks + check_matrix(matrix, [3, 4], (int, float)) + self.matrix=matrix + + def _dfs(self, groups): + """ + Deep first search that assign numbers to shapes + :param groups: dict(locations=[], curves_2d=[], curves_3d=[], surfaces=[], shapes=[]) + :return: None + """ + if not hasattr(self, 'id'): + id=len(groups['locations'])+1 + self.id = id + groups['locations'].append(self) + + def _brep_output(self, stream, groups): + stream.write("1\n") + for row in self.matrix: + for number in row: + stream.write(" {}".format(number)) + stream.write("\n") + +class ComposedLocation(Location): + """ + Defines an affine transformation as a composition of othr transformations. Corresponds to the in the BREP file. + BREP format allows to use different transformations for individual shapes. + """ + def __init__(self, location_powers=[]): + """ + + :param location_powers: List of pairs (location, power) where location is instance of Location and power is float. + """ + locs, pows = zip(*location_powers) + l = len(locs) + check_matrix(locs, [ l ], (Location, ComposedLocation) ) + check_matrix(pows, [ l ], int) + + self.location_powers=location_powers + + + def _dfs(self, groups): + + for location,power in self.location_powers: + location._dfs(groups) + Location._dfs(self, groups) + + def _brep_output(self, stream, groups): + stream.write("2 ") + for loc, pow in self.location_powers: + stream.write("{} {} ".format(loc.id, pow)) + stream.write("0\n") + +def check_knots(deg, knots, N): + total_multiplicity = 0 + for knot, mult in knots: + # This condition must hold if we assume only (0,1) interval of curve or surface parameters. + #assert float(knot) >= 0.0 and float(knot) <= 1.0 + total_multiplicity += mult + assert total_multiplicity == deg + N + 1 + + +# TODO: perform explicit conversion to np.float64 in order to avoid problems on different arch +# should be unified in bspline as well, convert to np.arrays as soon as posible +scalar_types = (int, float, np.int64, np.float64) + + +def curve_from_bs( curve): + """ + Make BREP writer Curve (2d or 3d) from bspline curve. + :param curve: bs.Curve object + :return: + """ + dim = curve.dim + if dim == 2: + curve_dim = Curve2D + elif dim == 3: + curve_dim = Curve3D + else: + assert False + return curve_dim(curve.poles, curve.basis.pack_knots(), curve.rational, curve.basis.degree) + +class Curve3D: + """ + Defines a 3D curve as B-spline. We shall work only with B-splines of degree 2. + Corresponds to "B-spline Curve - <3D curve record 7>" from BREP format description. + """ + def __init__(self, poles, knots, rational=False, degree=2): + """ + Construct a B-spline in 3d space. + :param poles: List of poles (control points) ( X, Y, Z ) or weighted points (X,Y,Z, w). X,Y,Z,w are floats. + Weighted points are used only for rational B-splines (i.e. nurbs) + :param knots: List of tuples (knot, multiplicity), where knot is float, t-parameter on the curve of the knot + and multiplicity is positive int. Total number of knots, i.e. sum of their multiplicities, must be + degree + N + 1, where N is number of poles. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. + :param degree: Positive int. + """ + + if rational: + check_matrix(poles, [None, 4], scalar_types ) + else: + check_matrix(poles, [None, 3], scalar_types) + N = len(poles) + check_knots(degree, knots, N) + + self.poles=poles + self.knots=knots + self.rational=rational + self.degree=degree + + def _dfs(self, groups): + if not hasattr(self, 'id'): + id = len(groups['curves_3d']) + 1 + self.id = id + groups['curves_3d'].append(self) + + + def _brep_output(self, stream, groups): + # writes b-spline curve + stream.write("7 {} 0 {} {} {} ".format(int(self.rational), self.degree, len(self.poles), len(self.knots))) + for pole in self.poles: + for value in pole: + stream.write(" {}".format(value)) + stream.write(" ") + for knot in self.knots: + for value in knot: + stream.write(" {}".format(value)) + stream.write(" ") + stream.write("\n") + +class Curve2D: + """ + Defines a 2D curve as B-spline. We shall work only with B-splines of degree 2. + Corresponds to "B-spline Curve - <2D curve record 7>" from BREP format description. + """ + def __init__(self, poles, knots, rational=False, degree=2): + """ + Construct a B-spline in 2d space. + :param poles: List of points ( X, Y ) or weighted points (X,Y, w). X,Y,w are floats. + Weighted points are used only for rational B-splines (i.e. nurbs) + :param knots: List of tuples (knot, multiplicity), where knot is float, t-parameter on the curve of the knot + and multiplicity is positive int. Total number of knots, i.e. sum of their multiplicities, must be + degree + N + 1, where N is number of poles. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. + :param degree: Positive int. + """ + + N = len(poles) + if rational: + check_matrix(poles, [N, 3], scalar_types ) + else: + check_matrix(poles, [N, 2], scalar_types) + check_knots(degree, knots, N) + + self.poles=poles + self.knots=knots + self.rational=rational + self.degree=degree + + def _dfs(self, groups): + if not hasattr(self, 'id'): + id = len(groups['curves_2d']) + 1 + self.id = id + groups['curves_2d'].append(self) + + def _brep_output(self, stream, groups): + # writes b-spline curve + stream.write("7 {} 0 {} {} {} ".format(int(self.rational), self.degree, len(self.poles), len(self.knots))) + for pole in self.poles: + for value in pole: + stream.write(" {}".format(value)) + stream.write(" ") + for knot in self.knots: + for value in knot: + stream.write(" {}".format(value)) + stream.write(" ") + stream.write("\n") + + + +def surface_from_bs(surf): + """ + Make BREP writer Surface from bspline surface. + :param surf: bs.Surface object + :return: + """ + return Surface(surf.poles, (surf.u_basis.pack_knots(), surf.v_basis.pack_knots()), + surf.rational, (surf.u_basis.degree, surf.v_basis.degree) ) + + +class Surface: + """ + Defines a B-spline surface in 3d space. We shall work only with B-splines of degree 2. + Corresponds to "B-spline Surface - < surface record 9 >" from BREP format description. + """ + def __init__(self, poles, knots, rational=False, degree=(2,2)): + """ + Construct a B-spline in 3d space. + :param poles: Matrix (list of lists) of Nu times Nv poles (control points). + Single pole is a points ( X, Y, Z ) or weighted point (X,Y,Z, w). X,Y,Z,w are floats. + Weighted points are used only for rational B-splines (i.e. nurbs) + :param knots: Tuple (u_knots, v_knots). Both u_knots and v_knots are lists of tuples + (knot, multiplicity), where knot is float, t-parameter on the curve of the knot + and multiplicity is positive int. For both U and V knot vector the total number of knots, + i.e. sum of their multiplicities, must be degree + N + 1, where N is number of poles. + :param rational: True for rational B-spline, i.e. NURB. Use weighted poles. BREP format have two independent flags + for U and V parametr, but only choices 0,0 and 1,1 have sense. + :param degree: (u_degree, v_degree) Both positive ints. + """ + + if rational: + check_matrix(poles, [None, None, 4], scalar_types ) + else: + check_matrix(poles, [None, None, 3], scalar_types) + + assert len(poles) > 0 + assert len(poles[0]) > 0 + self.Nu = len(poles) + self.Nv = len(poles[0]) + for row in poles: + assert len(row) == self.Nv + + assert (not rational and len(poles[0][0]) == 3) or (rational and len(poles[0][0]) == 4) + + (u_knots, v_knots) = knots + check_knots(degree[0], u_knots, self.Nu) + check_knots(degree[1], v_knots, self.Nv) + + self.poles=poles + self.knots=knots + self.rational=rational + self.degree=degree + + def _dfs(self, groups): + if not hasattr(self, 'id'): + id = len(groups['surfaces']) + 1 + self.id = id + groups['surfaces'].append(self) + + def _brep_output(self, stream, groups): + #writes b-spline surface + stream.write("9 {} {} 0 0 ".format(int(self.rational),int(self.rational))) #prints B-spline surface u or v rational flag - both same + for i in self.degree: #prints <_> + stream.write(" {}".format(i)) + (u_knots, v_knots) = self.knots + stream.write(" {} {} {} {} ".format(self.Nu, self.Nv, len(u_knots), len(v_knots))) + #prints <_> <_> <_> +# stream.write(" {}".format(self.poles)) #TODO: tohle smaz, koukam na format poles a chci: B-spline surface weight poles + for pole in self.poles: #TODO: check, takovy pokus o poles + for vector in pole: + for value in vector: + stream.write(" {}".format(value)) + stream.write(" ") + stream.write(" ") + for knot in u_knots: #prints B-spline surface u multiplicity knots + for value in knot: + stream.write(" {}".format(value)) + stream.write(" ") + for knot in v_knots: #prints B-spline surface v multiplicity knots + for value in knot: + stream.write(" {}".format(value)) + stream.write(" ") + stream.write("\n") + +class Approx: + """ + Approximation methods for B/splines of degree 2. + + """ + @classmethod + def plane(cls, vtxs): + """ + Returns B-spline surface of a plane given by 3 points. + We retun also list of UV coordinates of the given points. + :param vtxs: List of tuples (X,Y,Z) + :return: ( Surface, vtxs_uv ) + """ + assert len(vtxs) == 3, "n vtx: {}".format(len(vtxs)) + vtxs.append( (0,0,0) ) + vtxs = np.array(vtxs) + vv = vtxs[1] + vtxs[2] - vtxs[0] + vtx4 = [ vtxs[0], vtxs[1], vv, vtxs[2]] + (surf, vtxs_uv) = cls.bilinear(vtx4) + return (surf, [ vtxs_uv[0], vtxs_uv[1], vtxs_uv[3] ]) + + @classmethod + def bilinear(cls, vtxs): + """ + Returns B-spline surface of a bilinear surface given by 4 corner points. + We retun also list of UV coordinates of the given points. + :param vtxs: List of tuples (X,Y,Z) + :return: ( Surface, vtxs_uv ) + """ + assert len(vtxs) == 4, "n vtx: {}".format(len(vtxs)) + vtxs = np.array(vtxs) + def mid(*idx): + return np.mean( vtxs[list(idx)], axis=0) + + # v - direction v0 -> v2 + # u - direction v0 -> v1 + poles = [ [vtxs[0], mid(0, 3), vtxs[3]], + [mid(0,1), mid(0,1,2,3), mid(2,3)], + [vtxs[1], mid(1,2), vtxs[2]] + ] + knots = [(0.0, 3), (1.0, 3)] + surface = Surface(poles, (knots, knots)) + vtxs_uv = [ (0, 0), (1, 0), (1, 1), (0, 1) ] + return (surface, vtxs_uv) + + + + + @classmethod + def _line(cls, vtxs, overhang=0.0): + ''' + :param vtxs: List of tuples (X,Y) or (X,Y,Z) + :return: + ''' + assert len(vtxs) == 2 + vtxs = np.array(vtxs) + mid = np.mean(vtxs, axis=0) + poles = [ vtxs[0], mid, vtxs[1] ] + knots = [(0.0+overhang, 3), (1.0-overhang, 3)] + return (poles, knots) + + @classmethod + def line_2d(cls, vtxs): + """ + Return B-spline approximation of line from two 2d points + :param vtxs: [ (X0, Y0), (X1, Y1) ] + :return: Curve2D + """ + return Curve2D( *cls._line(vtxs) ) + + @classmethod + def line_3d(cls, vtxs): + """ + Return B-spline approximation of line from two 3d points + :param vtxs: [ (X0, Y0, Z0), (X1, Y1, Z0) ] + :return: Curve2D + """ + return Curve3D(*cls._line(vtxs)) + + +class Orient(enum.IntEnum): + Forward=1 + Reversed=2 + Internal=3 + External=4 + +#op=Orient.Forward +#om=Orient.Reversed +#oi=Orient.Internal +#oe=Orient.External + +class ShapeRef: + """ + Auxiliary data class to store an object with its orientation + and possibly location. Meaning of location in this context is not clear yet. + Identity location (0) in all BREPs produced by OCC. + All methods accept the tuple (shape, orient, location) and + construct the ShapeRef object automatically. + """ + + orient_chars = ['+', '-', 'i', 'e'] + + def __init__(self, shape, orient=Orient.Forward, location=Location()): + """ + :param shape: referenced shape + :param orient: orientation of the shape, value is enum Orient + :param location: A Location object. Default is None = identity location. + """ + if not issubclass(type(shape), Shape): + raise ParamError("Expected Shape, get: {}.".format(shape)) + assert isinstance(orient, Orient) + assert issubclass(type(location), Location) + + self.shape=shape + self.orientation=orient + self.location=location + + def _writeformat(self, stream, groups): + + stream.write("{}{} {} ".format(self.orient_chars[self.orientation-1], self.shape.id, self.location.id)) + + def __repr__(self): + return "{}{} ".format(self.orient_chars[self.orientation-1], self.shape.id) + + +class ShapeFlag(dict): + """ + Auxiliary data class representing the shape flag word of BREP shapes. + All methods set the flags automatically, but it can be overwritten. + + Free - Seems to indicate a top level shapes. + Modified - ?? + Checked - for format version 2 may indicate that shape topology is already checked + Orientable - ?? + Closed - used to indicate closed Wires and Shells + Infinite - ?? may indicate shapes extending to infinite, not our case + Convex - ?? may indicate convexity of the shape, not clear how this is combined with geometry + """ + flag_names = ['free', 'modified', 'checked', 'orientable', 'closed', 'infinite', 'convex'] + + def __init__(self, *args): + for k, f in zip(self.flag_names, args): + assert f in [0, 1] + self[k]=f + + def set(self, key, value=1): + if value: + value =1 + else: + value =0 + self[key] = value + + def unset(self, key): + self[key] = 0 + + def _brep_output(self, stream): + for k in self.flag_names: + stream.write(str(self[k])) + +class Shape: + def __init__(self, childs): + """ + Construct base Shape object. + Examples: + Wire([ edge_1, edge_2.m(), edge_3]) # recommended + Wire(edge_1, ShapeRef(edge_2, Orient.Reversed, some_location), edge_3) + ... not recommended since it is bad idea to reference same shape with different Locations. + + :param childs: List of ShapeRefs or child objects. + """ + + # self.subtypes - List of allowed types of childs. + assert hasattr(self, 'sub_types'), self + + # convert list of shape reference tuples to ShapeRef objects + # automaticaly wrap naked shapes into tuple. + self.childs=[] + for child in childs: + self.append(child) # append convert to ShapeRef + + # Thes flags are usualy produced by OCC for all other shapes safe vertices. + self.flags=ShapeFlag(0,1,0,1,0,0,0) + + # self.shpname: Shape name, defined in childs + assert hasattr(self, 'shpname'), self + + + """ + Methods to simplify ceration of oriented references. + """ + def p(self): + return ShapeRef(self, Orient.Forward) + + def m(self): + return ShapeRef(self, Orient.Reversed) + + def i(self): + return ShapeRef(self, Orient.Internal) + + def e(self): + return ShapeRef(self, Orient.External) + + def subshapes(self): + # Return list of subshapes stored in child ShapeRefs. + return [chld.shape for chld in self.childs] + + def append(self, shape_ref): + """ + Append a reference to shild + :param shape_ref: Either ShapeRef or child shape. + :return: None + """ + if type(shape_ref) != ShapeRef: + shape_ref=ShapeRef(shape_ref) + if not isinstance(shape_ref.shape, tuple(self.sub_types)): + raise ParamError("Wrong child type: {}, allowed: {}".format(type(shape_ref.shape), self.sub_types)) + self.childs.append(shape_ref) + + #def _convert_to_shaperefs(self, childs): + + + def set_flags(self, flags): + """ + Set flags given as tuple. + :param flags: Tuple of 7 flags. + :return: + """ + self.flags = ShapeFlag(*flags) + + + def is_closed(self): + return self.flags['closed'] + + + def _dfs(self, groups): + """ + Deep first search that assign numbers to shapes + :param groups: dict(locations=[], curves_2d=[], curves_3d=[], surfaces=[], shapes=[]) + :return: None + """ + if hasattr(self, 'id'): + return + for sub_ref in self.childs: + sub_ref.location._dfs(groups) + sub_ref.shape._dfs(groups) + groups['shapes'].append(self) + self.id = len(groups['shapes']) + + def _brep_output(self, stream, groups): + stream.write("{}\n".format(self.shpname)) + self._subrecordoutput(stream) + self.flags._brep_output(stream) + stream.write("\n") +# stream.write("{}".format(self.childs)) + for child in self.childs: + child._writeformat(stream, groups) + stream.write("*\n") + #subshape, tj. childs + + def _subrecordoutput(self, stream): + stream.write("\n") + + def __repr__(self): + if not hasattr(self, 'id'): + self.index_all() + if len(self.childs)==0: + return "" + repr = "" + repr+=self.shpname + " " + str(self.id) + " : " + for child in self.childs: + repr+=child.__repr__() + repr+="\n" + for child in self.childs: + repr+=child.shape.__repr__() + repr+="\n" + return repr + + + def index_all(self, location=Location()): + # print("Index") + # print(compound.__class__.__name__) #prints class name + + groups = dict(locations=[], curves_2d=[], curves_3d=[], surfaces=[], shapes=[]) + self._dfs(groups) # pridej jako parametr dictionary listu jednotlivych grup. v listech primo objekty + location._dfs(groups) + # print(groups) + return groups + + +""" +Shapes with no special parameters, only flags and subshapes. +Writer can be generic implemented in bas class Shape. +""" + +class Compound(Shape): + def __init__(self, shapes=[]): + self.sub_types = [CompoundSolid, Solid, Shell, Wire, Face, Edge, Vertex] + self.shpname = 'Co' + super().__init__(shapes) + #flags: free, modified, IGNORED, orientable, closed, infinite, convex + self.set_flags( (1, 1, 0, 0, 0, 0, 0) ) # free, modified + + def set_free_shapes(self): + """ + Set 'free' attributes to all shapes of the compound. + :return: + """ + for shape in self.subshapes(): + shape.flags.set('free', True) + + +class CompoundSolid(Shape): + def __init__(self, solids=[]): + self.sub_types = [Solid] + self.shpname = 'Cs' + super().__init__(solids) + + +class Solid(Shape): + def __init__(self, shells=[]): + self.sub_types = [Shell] + self.shpname='So' + super().__init__(shells) + self.set_flags((0, 1, 0, 0, 0, 0, 0)) # modified + +class Shell(Shape): + def __init__(self, faces=[]): + self.sub_types = [Face] + self.shpname='Sh' + super().__init__(faces) + self.set_flags((0, 1, 0, 1, 0, 0, 0)) # modified, orientable + + +class Wire(Shape): + def __init__(self, edges=[]): + self.sub_types = [Edge] + self.shpname='Wi' + super().__init__(edges) + self.set_flags((0, 1, 0, 1, 0, 0, 0)) # modified, orientable + self._set_closed() + + def _set_closed(self): + ''' + Return true for the even parity of vertices. + :return: REtrun true if wire is closed. + ''' + vtx_set = {} + for edge in self.subshapes(): + for vtx in edge.subshapes(): + vtx_set[vtx] = 0 + vtx.n_edges += 1 + closed = True + for vtx in vtx_set.keys(): + if vtx.n_edges % 2 != 0: + closed = False + vtx.n_edges = 0 + self.flags.set('closed', closed) + + +""" +Shapes with special parameters. +Specific writers are necessary. +""" + +class Face(Shape): + """ + Face class. + Like vertex and edge have some additional parameters in the BREP format. + """ + + def __init__(self, wires, surface=None, location=Location(), tolerance=1.0e-3): + """ + :param wires: List of wires, or list of edges, or list of ShapeRef tuples of Edges to construct a Wire. + :param surface: Representation of the face, surface on which face lies. + :param location: Location of the surface. + :param tolerance: Tolerance of the representation. + """ + self.sub_types = [Wire, Edge] + self.tol=tolerance + self.restriction_flag =0 + self.shpname = 'Fa' + + if type(wires) != list: + wires = [ wires ] + assert(len(wires) > 0) + super().__init__(wires) + + # auto convert list of edges into wire + shape_type = type(self.childs[0].shape) + for shape in self.subshapes(): + assert type(shape) == shape_type + if shape_type == Edge: + wire = Wire(self.childs) + self.childs = [] + self.append(wire) + + # check that wires are closed + for wire in self.subshapes(): + if not wire.is_closed(): + raise Exception("Trying to make face from non-closed wire.") + + if surface is None: + self.repr=[] + else: + assert type(surface) == Surface + self.repr=[(surface, location)] + + + + def _dfs(self, groups): + Shape._dfs(self,groups) + if not self.repr: + self.implicit_surface() + assert len(self.repr) == 1 + for repr, loc in self.repr: + repr._dfs(groups) + loc._dfs(groups) + + # update geometry representation of edges (add 2D curves) + for wire in self.subshapes(): + for edge in wire.subshapes(): + edge._dfs(groups) + + + def implicit_surface(self): + """ + Construct a surface if surface is None. Works only for + 3 and 4 vertices (plane or bilinear surface) + Should be called in _dfs just after all child shapes are passed. + :return: None + """ + edges = {} + vtxs = [] + for wire in self.subshapes(): + for edge in wire.childs: + edges[edge.shape.id] = edge.shape + e_vtxs = edge.shape.subshapes() + if edge.orientation == Orient.Reversed: + e_vtxs.reverse() + for vtx in e_vtxs: + vtxs.append( (vtx.id, vtx.point) ) + vtxs = vtxs[1:] + vtxs[:1] + odd_vtx = vtxs[1::2] + even_vtx = vtxs[0::2] + assert odd_vtx == even_vtx, "odd: {} even: {}".format(odd_vtx, even_vtx) + vtxs = odd_vtx + if len(vtxs) == 3: + constructor = Approx.plane + elif len(vtxs) == 4: + constructor = Approx.bilinear + else: + raise Exception("Too many vertices {} for implicit surface construction.".format(len(vtxs))) + (ids, points) = zip(*vtxs) + (surface, vtxs_uv) = constructor(list(points)) + self.repr = [(surface, Location())] + + # set representation of edges + assert len(ids) == len(vtxs_uv) + id_to_uv = dict(zip(ids, vtxs_uv)) + for edge in edges.values(): + e_vtxs = edge.subshapes() + v0_uv = id_to_uv[e_vtxs[0].id] + v1_uv = id_to_uv[e_vtxs[1].id] + edge.attach_to_plane( surface, v0_uv, v1_uv ) + + # TODO: Possibly more general attachment of edges to 2D curves for general surfaces, but it depends + # on organisation of intersection curves. + + def _subrecordoutput(self, stream): + assert len(self.repr) == 1 + surf,loc = self.repr[0] + stream.write("{} {} {} {}\n\n".format(self.restriction_flag,self.tol,surf.id,loc.id)) + + +class Edge(Shape): + """ + Edge class. Special edge flags have unclear meaning. + Allow setting representations of the edge, this is crucial for good mash generation. + """ + + class Repr(enum.IntEnum): + Curve3d = 1 + Curve2d = 2 + #Continuous2d=3 + + + def __init__(self, vertices, tolerance=1.0e-3): + """ + :param vertices: List of shape reference tuples, see ShapeRef class. + :param tolerance: Tolerance of the representation. + """ + self.sub_types = [Vertex] + self.shpname = 'Ed' + self.tol = tolerance + self.repr = [] + self.edge_flags=(1,1,0) # this is usual value + + assert(len(vertices) == 2) + + super().__init__(vertices) + # Overwrite vertex orientation + self.childs[0].orientation = Orient.Forward + self.childs[1].orientation = Orient.Reversed + + def set_edge_flags(self, same_parameter, same_range, degenerated): + """ + Edge flags with unclear meaning. + :param same_parameter: + :param same_range: + :param degenerated: + :return: + """ + self.edge_flags=(same_parameter,same_range, degenerated) + + def points(self): + ''' + :return: List of coordinates of the edge vertices. + ''' + return [ vtx.point for vtx in self.subshapes()] + + def attach_to_3d_curve(self, t_range, curve, location=Location()): + """ + Add vertex representation on a 3D curve. + :param t_range: Tuple (t_min, t_max). + :param curve: 3D curve object (Curve3d) + :param location: Location object. Default is None = identity location. + :return: None + """ + assert type(curve) == Curve3D + self.repr.append( (self.Repr.Curve3d, t_range, curve, location) ) + + def attach_to_2d_curve(self, t_range, curve, surface, location=Location()): + """ + Add vertex representation on a 2D curve. + :param t_range: Tuple (t_min, t_max). + :param curve: 2D curve object (Curve2d) + :param surface: Surface on which the curve lies. + :param location: Location object. Default is None = identity location. + :return: None + """ + assert type(surface) == Surface + assert type(curve) == Curve2D + self.repr.append( (self.Repr.Curve2d, t_range, curve, surface, location) ) + + def attach_to_plane(self, surface, v0, v1): + """ + Construct and attach 2D line in UV space of the 'surface' + :param surface: A Surface object. + :param v0: UV coordinate of the first edge point + :param v1: UV coordinate of the second edge point + :return: + """ + assert type(surface) == Surface + self.attach_to_2d_curve((0.0, 1.0), Approx.line_2d([v0, v1]), surface) + + def implicit_curve(self): + """ + Construct a line 3d curve if there is no 3D representation. + Should be called in _dfs. + :return: + """ + vtx_points = self.points() + self.attach_to_3d_curve((0.0,1.0), Approx.line_3d( vtx_points )) + + #def attach_continuity(self): + + def _dfs(self, groups): + Shape._dfs(self,groups) + if not self.repr: + self.implicit_curve() + assert len(self.repr) > 0 + for i,repr in enumerate(self.repr): + if repr[0]==self.Repr.Curve2d: + repr[2]._dfs(groups) #curve + repr[3]._dfs(groups) #surface + repr[4]._dfs(groups) #location + elif repr[0]==self.Repr.Curve3d: + repr[2]._dfs(groups) #curve + repr[3]._dfs(groups) #location + + def _subrecordoutput(self, stream): #prints edge data #TODO: tisknu nekolik data representation + assert len(self.repr) > 0 + stream.write(" {} {} {} {}\n".format(self.tol,self.edge_flags[0],self.edge_flags[1],self.edge_flags[2])) + for i,repr in enumerate(self.repr): + if repr[0] == self.Repr.Curve2d: + curve_type, t_range, curve, surface, location = repr + stream.write("2 {} {} {} {} {}\n".format(curve.id, surface.id, location.id,t_range[0],t_range[1] )) #TODO: 2 <_> <_> + elif repr[0] == self.Repr.Curve3d: + curve_type, t_range, curve, location = repr + stream.write("1 {} {} {} {}\n".format(curve.id, location.id, t_range[0], t_range[1])) #TODO: 3 + stream.write("0\n") + +class Vertex(Shape): + """ + Vertex class. + Allow setting representations of the vertex but seems it is not used in BREPs produced by OCC. + """ + + class Repr(enum.IntEnum): + Curve3d = 1 + Curve2d = 2 + Surface = 3 + + def __init__(self, point, tolerance=1.0e-3): + """ + :param point: 3d point (X,Y,Z) + :param tolerance: Tolerance of the representation. + """ + check_matrix(point, [3], scalar_types) + + # These flags are produced by OCC for vertices. + self.flags = ShapeFlag(0, 1, 0, 1, 1, 0, 1) + # Coordinates in the 3D space. [X, Y, Z] + self.point=np.array(point) + # tolerance of representations. + self.tolerance=tolerance + # List of geometrical representations of the vertex. Possibly not necessary for meshing. + self.repr=[] + # Number of edges in which vertex is used. Used internally to check closed wires. + self.n_edges = 0 + self.shpname = 'Ve' + self.sub_types=[] + + super().__init__(childs=[]) + + def attach_to_3d_curve(self, t, curve, location=Location()): + """ + Add vertex representation on a 3D curve. + :param t: Parameter of the point on the curve. + :param curve: 3D curve object (Curve3d) + :param location: Location object. Default is None = identity location. + :return: None + """ + self.repr.append( (self.Repr.Curve3d, t, curve, location) ) + + def attach_to_2d_curve(self, t, curve, surface, location=Location()): + """ + Add vertex representation on a 2D curve on a surface. + :param t: Parameter of the point on the curve. + :param curve: 2D curve object (Curve2d) + :param surface: Surface on which the curve lies. + :param location: Location object. Default is None = identity location. + :return: None + """ + self.repr.append( (self.Repr.Curve2d, t, curve, surface, location) ) + + def attach_to_surface(self, u, v, surface, location=Location()): + """ + Add vertex representation on a 3D curve. + :param u,v: Parameters u,v of the point on the surface. + :param surface: Surface object. + :param location: Location object. Default is None = identity location. + :return: None + """ + self.repr.append( (self.Repr.Surface, u,v, surface, location) ) + + def _dfs(self, groups): + Shape._dfs(self,groups) + for repr in self.repr: + if repr[0]==self.Repr.Curve2d: + repr[2]._dfs(groups) #curve + repr[3]._dfs(groups) #surface + repr[4]._dfs(groups) #location + elif repr[0]==self.Repr.Curve3d: + repr[2]._dfs(groups) #curve + repr[3]._dfs(groups) #location + + + def _subrecordoutput(self, stream): #prints vertex data + stream.write("{}\n".format(self.tolerance)) + for i in self.point: + stream.write("{} ".format(i)) + stream.write("\n0 0\n\n") #no added + + + + +def write_model(stream, compound, location): + + groups = compound.index_all(location=location) + + # fix shape IDs + n_shapes = len(groups['shapes']) + 1 + for shape in groups['shapes']: + shape.id = n_shapes - shape.id + + stream.write("DBRep_DrawableShape\n\n") + stream.write("CASCADE Topology V1, (c) Matra-Datavision\n") + stream.write("Locations {}\n".format(len(groups['locations']))) + for loc in groups['locations']: + loc._brep_output(stream, groups) + + stream.write("Curve2ds {}\n".format(len(groups['curves_2d']))) + for curve in groups['curves_2d']: + curve._brep_output(stream, groups) + + stream.write("Curves {}\n".format(len(groups['curves_3d']))) + for curve in groups['curves_3d']: + curve._brep_output(stream, groups) + + stream.write("Polygon3D 0\n") + + stream.write("PolygonOnTriangulations 0\n") + + stream.write("Surfaces {}\n".format(len(groups['surfaces']))) + for surface in groups['surfaces']: + surface._brep_output(stream, groups) + + stream.write("Triangulations 0\n") + + stream.write("\nTShapes {}\n".format(len(groups['shapes']))) + for shape in groups['shapes']: + #print("# {} id: {} childs: {}\n".format(shape.shpname, shape.id, + # [ch.id for ch in shape.subshapes()])) + shape._brep_output(stream, groups) + stream.write("\n+1 0") + #stream.write("0\n") + + + From 25ef5838a4c243716424ff3c367d19dd2eafb6bf Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 21 Sep 2017 17:02:56 +0200 Subject: [PATCH 31/36] Fix grid surf load, add surface.aabb, test for brep writer --- src/brep_writer.py | 3 +- src/bspline.py | 76 +++--------------- src/bspline_approx.py | 2 + tests/test_brep_writer.py | 157 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 68 deletions(-) create mode 100644 tests/test_brep_writer.py diff --git a/src/brep_writer.py b/src/brep_writer.py index 1edadaa..3888434 100644 --- a/src/brep_writer.py +++ b/src/brep_writer.py @@ -1,7 +1,6 @@ import enum -import itertools import numpy as np -import bspline as bs + ''' diff --git a/src/bspline.py b/src/bspline.py index ceb5259..c562083 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -20,69 +20,6 @@ __author__ = 'Jan Brezina , Jiri Hnidek , Jiri Kopal ' - -# class ParamError(Exception): -# pass -# -# def check_matrix(mat, shape, values, idx=[]): -# ''' -# Check shape and type of scalar, vector or matrix. -# :param mat: Scalar, vector, or vector of vectors (i.e. matrix). Vector may be list or other iterable. -# :param shape: List of dimensions: [] for scalar, [ n ] for vector, [n_rows, n_cols] for matrix. -# If a value in this list is None, the dimension can be arbitrary. The shape list is set fo actual dimensions -# of the matrix. -# :param values: Type or tuple of allowed types of elements of the matrix. E.g. ( int, float ) -# :param idx: Internal. Used to pass actual index in the matrix for possible error messages. -# :return: -# ''' -# try: -# if len(shape) == 0: -# if not isinstance(mat, values): -# raise ParamError("Element at index {} of type {}, expected instance of {}.".format(idx, type(mat), values)) -# else: -# if shape[0] is None: -# shape[0] = len(mat) -# l=None -# if not hasattr(mat, '__len__'): -# l=0 -# elif len(mat) != shape[0]: -# l=len(mat) -# if not l is None: -# raise ParamError("Wrong len {} of element {}, should be {}.".format(l, idx, shape[0])) -# for i, item in enumerate(mat): -# sub_shape = shape[1:] -# check_matrix(item, sub_shape, values, idx = [i] + idx) -# shape[1:] = sub_shape -# return shape -# except ParamError: -# raise -# except Exception as e: -# raise ParamError(e) -# -# -# def check_knots(deg, knots, N): -# total_multiplicity = 0 -# for knot, mult in knots: -# # This condition must hold if we assume only (0,1) interval of curve or surface parameters. -# #assert float(knot) >= 0.0 and float(knot) <= 1.0 -# total_multiplicity += mult -# assert total_multiplicity == deg + N + 1 -# -# -# scalar_types = (int, float, np.int64) - - - - - - - - - - - - - class SplineBasis: """ @@ -496,7 +433,7 @@ def make_raw(cls, poles, knots, rational=False, degree=(2,2)): :param degree: Non-negative int """ u_basis = SplineBasis(degree[0], knots[0]) - v_basis = SplineBasis(degree[0], knots[0]) + v_basis = SplineBasis(degree[1], knots[1]) return cls( (u_basis, v_basis), poles, rational) @@ -567,6 +504,13 @@ def eval_array(self, uv_points): assert uv_points.shape[1] == 2 return np.array( [ self.eval(u, v) for u, v in uv_points] ) + def aabb(self): + """ + Return Axes Aligned Bounding Box of the poles, which should be also bounding box of the curve itself. + :return: ( min_corner, max_corner); Box corners are numpy arryas of dimension D. + TODO: test + """ + return (np.amin(self.poles, axis=(0,1)), np.amax(self.poles, axis=(0,1))) class Z_Surface: @@ -777,7 +721,7 @@ class GridSurface: @staticmethod - def load(self, filename): + def load(filename): """ Load the grid surface from file :param filename: @@ -785,7 +729,7 @@ def load(self, filename): """ point_seq = np.loadtxt(filename) assert min(point_seq.shape) > 1 - GridSurface(point_seq.T) + return GridSurface(point_seq) """ Can load and check grid of XYZ points and construct a diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 7b60048..0cf6f5f 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -20,6 +20,8 @@ def plane_surface(vtxs, overhang=0.0): """ Returns B-spline surface of a plane given by 3 points. We retun also list of UV coordinates of the given points. + U direction v0 -> v1 + V direction v0 -> v2 :param vtxs: List of tuples (X,Y,Z) :return: ( Surface, vtxs_uv ) """ diff --git a/tests/test_brep_writer.py b/tests/test_brep_writer.py new file mode 100644 index 0000000..598435f --- /dev/null +++ b/tests/test_brep_writer.py @@ -0,0 +1,157 @@ +import pytest +import unittest +import brep_writer as bw +import sys + + + +class TestConstructors(unittest.TestCase): + def test_Location(self): + print( "test locations") + la = bw.Location([[1, 0, 0, 4], [0, 1, 0, 8], [0, 0, 1, 12]]) + lb = bw.Location([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + lc = bw.ComposedLocation([ (la, 2), (lb, 1) ]) + ld = bw.ComposedLocation([ (lb, 2), (lc, 3) ]) + with self.assertRaises(bw.ParamError): + bw.Location([1,2,3]) + with self.assertRaises(bw.ParamError): + bw.Location([[1], [2], [3]]) + with self.assertRaises(bw.ParamError): + a = 1 + b = 'a' + lb = bw.Location([[a, b, a, b], [a, b, a, b], [a, b, a, b]]) + + def test_Shape(self): + + # check sub types + with self.assertRaises(bw.ParamError): + bw.Wire(['a', 'b']) + v=bw.Vertex([1,2,3]) + with self.assertRaises(bw.ParamError): + bw.Wire([v, v]) + + + def test_Vertex(self): + with self.assertRaises(bw.ParamError): + bw.Vertex(['a','b','c']) + + +class TestPlanarGeomeries(unittest.TestCase): + + def _cube(self): + # 0, 0; top, bottom + v1=bw.Vertex((0.0, 0.0, 1.0)) + v2=bw.Vertex((0.0, 0.0, 0.0)) + + v3=bw.Vertex((0.0, 1.0, 1.0)) + v4=bw.Vertex((0.0, 1.0, 0.0)) + + v5=bw.Vertex((1.0, 0.0, 1.0)) + v6=bw.Vertex((1.0, 0.0, 0.0)) + + # vertical edges + e1=bw.Edge([v1,v2]) + e2=bw.Edge([v3,v4]) + e3=bw.Edge([v5,v6]) + + # face v12 - v34 + # top + e4=bw.Edge([v1,v3]) + # bottom + e5=bw.Edge([v2,v4]) + f1 = bw.Face([e1.m(), e4, e2, e5.m()]) + + # face v34 - v56 + # top + e6=bw.Edge([v3, v5]) + # bottom + e7=bw.Edge([v4, v6]) + f2 = bw.Face([e2.m(), e6, e3, e7.m()]) + + # face v56 - v12 + # top + e8=bw.Edge([v5, v1]) + # bottom + e9=bw.Edge([v6, v2]) + f3 = bw.Face([e3.m(), e8, e1, e9.m()]) + + # top cup + f4 = bw.Face([e4, e6, e8]) + # bot cup + w5=bw.Wire([e5, e7, e9]) + f5 = bw.Face([w5.m()]) + + shell = bw.Shell([ f1, f2, f3, f4, f5.m() ]) + return shell + + def _permuted_cube(self): + # 0, 0; top, bottom + v1=bw.Vertex((0.0, 0.0, 1.0)) + v2=bw.Vertex((0.0, 0.0, 0.0)) + + v3=bw.Vertex((0.0, 1.0, 1.0)) + v4=bw.Vertex((0.0, 1.0, 0.0)) + + v5=bw.Vertex((1.0, 0.0, 1.0)) + v6=bw.Vertex((1.0, 0.0, 0.0)) + + # face v12 - v34 + # top + e4=bw.Edge([v1,v3]) + # top + e6=bw.Edge([v3, v5]) + # top + e8=bw.Edge([v5, v1]) + + # top cup + f4 = bw.Face([e4, e6, e8]) + + # bottom + e5=bw.Edge([v2,v4]) + # face v34 - v56 + # bottom + e7=bw.Edge([v4, v6]) + # face v56 - v12 + # bottom + e9=bw.Edge([v6, v2]) + + # bot cup + w5=bw.Wire([e5, e7, e9]) + f5 = bw.Face([w5.m()]) + + + # vertical edges + e1=bw.Edge([v1,v2]) + e2=bw.Edge([v3,v4]) + e3=bw.Edge([v5,v6]) + + f1 = bw.Face([e1.m(), e4, e2, e5.m()]) + + f2 = bw.Face([e2.m(), e6, e3, e7.m()]) + + f3 = bw.Face([e3.m(), e8, e1, e9.m()]) + + shell = bw.Shell([ f4, f5.m(), f1, f2, f3 ]) + return shell + + def test_cube(self): + + shell = self._cube() + #shell = self._permuted_cube() + + s1=bw.Solid([ shell ]) + + c1=bw.Compound([s1]) + + loc1=bw.Location([[0,0,1,0],[1,0,0,0],[0,1,0,0]]) + loc2=bw.Location([[0,0,1,0],[1,0,0,0],[0,1,0,0]]) #dej tam tu druhou z prikladu + cloc=bw.ComposedLocation([(loc1,1),(loc2,1)]) + + with open("_out_test_prism.brep", "w") as f: + bw.write_model(f, c1, cloc) + #bw.write_model(sys.stdout, c1, cloc) + print(c1) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 7e2900cf3b657d6ccbc247755b281d922c2e47c3 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Thu, 21 Sep 2017 18:41:57 +0200 Subject: [PATCH 32/36] Implement aabb and grid center. --- src/bspline.py | 16 +++++++++++++-- tests/test_bspline.py | 46 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index c562083..b2377a3 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -412,7 +412,7 @@ def aabb(self): Return Axes Aligned Bounding Box of the poles, which should be also bounding box of the curve itself. :return: ( min_corner, max_corner); Box corners are numpy arryas of dimension D. """ - return (np.amin(self.poles, axis=0), np.amax(self.poles, axis=0)) + return np.array( [np.amin(self.poles, axis=0), np.amax(self.poles, axis=0)] ) class Surface: @@ -510,7 +510,7 @@ def aabb(self): :return: ( min_corner, max_corner); Box corners are numpy arryas of dimension D. TODO: test """ - return (np.amin(self.poles, axis=(0,1)), np.amax(self.poles, axis=(0,1))) + return np.array( [np.amin(self.poles, axis=(0,1)), np.amax(self.poles, axis=(0,1))] ) class Z_Surface: @@ -706,6 +706,13 @@ def z_eval_xy_array(self, xy_points): return z_points.reshape(-1) + def aabb(self): + xyz_box = np.empty( (2, 3) ) + xyz_box[0, 0:2] = np.amin(self.quad, axis=0) + xyz_box[1, 0:2] = np.amax(self.quad, axis=0) + xyz_box[:, 2] = self.z_surface.aabb()[:,0] + return xyz_box + class GridNotInShapeExc(Exception): @@ -926,8 +933,13 @@ def z_eval_array(self, uv_points): return result + def center(self): + return self.eval_array( np.array([ [0.5, 0.5] ]))[0] + def aabb(self): + return self.z_surface.aabb() + def make_function_grid(fn, nu, nv): """ diff --git a/tests/test_bspline.py b/tests/test_bspline.py index b488d73..762f8cc 100644 --- a/tests/test_bspline.py +++ b/tests/test_bspline.py @@ -215,7 +215,12 @@ def test_evaluate(self): pass - + def test_aabb(self): + poles = [ [0., 0.], [1.0, 0.5], [2., -2.], [3., 1.] ] + basis = bs.SplineBasis.make_equidistant(2, 2) + curve = bs.Curve(basis, poles) + box = curve.aabb() + assert np.allclose( box, np.array([ [0,-2], [3, 1]]) ) @@ -242,13 +247,13 @@ def plot_extrude(self): plt.show() def plot_function(self): + fig = plt.figure() + ax = fig.gca(projection='3d') + # function surface def function(x): return math.sin(x[0]) * math.cos(x[1]) - fig = plt.figure() - ax = fig.gca(projection='3d') - poles = bs.make_function_grid(function, 4, 5) u_basis = bs.SplineBasis.make_equidistant(2, 2) v_basis = bs.SplineBasis.make_equidistant(2, 3) @@ -265,6 +270,19 @@ def test_evaluate(self): pass + def test_aabb(self): + # function surface + def function(x): + return x[0] * (x[1] + 1.0) + 3.0 + + poles = bs.make_function_grid(function, 4, 5) + u_basis = bs.SplineBasis.make_equidistant(2, 2) + v_basis = bs.SplineBasis.make_equidistant(2, 3) + surface_func = bs.Surface( (u_basis, v_basis), np.array(poles)) + box = surface_func.aabb() + assert np.allclose( box, np.array([ [0,0, 3], [1, 1, 5]]) ) + + class TestZ_Surface: @@ -299,6 +317,20 @@ def test_eval_uv(self): #self.plot_function_uv() pass + def test_aabb(self): + # function surface + def function(x): + return x[0] * (x[1] + 1.0) + 3.0 + + poles = bs.make_function_grid(function, 4, 5) + u_basis = bs.SplineBasis.make_equidistant(2, 2) + v_basis = bs.SplineBasis.make_equidistant(2, 3) + surface_func = bs.Surface( (u_basis, v_basis), poles[:,:, [2] ]) + + quad = np.array( [ [0, 0], [0, 0.5], [1, 0.1], [1.1, 1.1] ] ) + z_surf = bs.Z_Surface(quad, surface_func) + box = z_surf.aabb() + assert np.allclose(box, np.array([[0, 0, 3], [1.1, 1.1, 5]])) class TestPointGrid: @@ -389,4 +421,8 @@ def test_grid_surface(self): assert np.all(surface.quad == new_quad), "surf: {} ref: {}".format(surface.quad, new_quad) self.check_surface(surface, xy_mat, xy_shift, z_shift) - + surf_center = surface.center() + ref_center = np.array( [-2.0, math.sqrt(2.0)+5.0, (math.sin(0.5)*math.cos(0.5) + 1.3) ] ) + #print(surf_center) + #print(ref_center) + assert np.allclose( ref_center, surf_center, rtol=0.01) From 3d0d1de523cff14f0191415e0ec249901a95e3f1 Mon Sep 17 00:00:00 2001 From: jiri Date: Wed, 27 Sep 2017 13:15:44 +0200 Subject: [PATCH 33/36] construction of the matrix A was optimized --- src/bspline_approx.py | 54 ++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 0cf6f5f..777ba65 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -336,7 +336,7 @@ def _basis_in_q_points(self, basis): nq_points = len(self._q_points) point_val = np.zeros((3, n_int * nq_points)) d_point_val = np.zeros((3, n_int * nq_points)) - point_idx = np.zeros((n_int * nq_points, 1)) + point_idx = np.zeros((n_int, 1)) q_point = np.zeros((n_int * nq_points, 1)) n = 0 @@ -352,8 +352,8 @@ def _basis_in_q_points(self, basis): u_base_vec_diff = basis.eval_diff_base_vector(idx, up) point_val[:, n] = u_base_vec d_point_val[:, n] = u_base_vec_diff - point_idx[n] = idx n += 1 + point_idx[i] = idx return point_val, d_point_val, point_idx, q_point def build_sparse_reg_matrix(self): @@ -385,38 +385,39 @@ def build_sparse_reg_matrix(self): self._weights = [1.0 / 6, 5.0 / 6, 5.0 / 6, 1.0 / 6] nq_points = len(self._q_points) - # TODO: rename: u_vals, u_diffs, u_idxs, u_points - u_point_val, ud_point_val, u_point_idx, q_u_point = self._basis_in_q_points(self.u_basis) - v_point_val, vd_point_val, v_point_idx, q_v_point = self._basis_in_q_points(self.v_basis) + # DONE: rename: u_vals, u_diffs, u_idxs, u_points + u_val, ud_val, u_idx, q_u_point = self._basis_in_q_points(self.u_basis) + v_val, vd_val, v_idx, q_v_point = self._basis_in_q_points(self.v_basis) # Matrix construction - # TODO: Assembly local dense blocks 9*9 and then put these nonzeroes into sparse matirx + # DONE: Assembly local dense blocks 9*9 and then put these nonzeroes into sparse matirx # TODO: use numpy opperations to make assembly of local blocks readable, eliminate loops colv = np.zeros(n_uv_loc_nz) - # TODO:rename data and data2 to something meqningful - data = np.zeros(n_uv_loc_nz) - data2 = np.zeros(n_uv_loc_nz) - row_m = np.zeros((v_n_inter * u_n_inter * nq_points * nq_points * n_uv_loc_nz * n_uv_loc_nz)) - col_m = np.zeros((v_n_inter * u_n_inter * nq_points * nq_points * n_uv_loc_nz * n_uv_loc_nz)) - data_m = np.zeros((v_n_inter * u_n_inter * nq_points * nq_points * n_uv_loc_nz * n_uv_loc_nz)) + # DONE:rename data and data2 to something meqningful + v_diff_u = np.zeros(n_uv_loc_nz) + v_u_diff = np.zeros(n_uv_loc_nz) + row_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) + col_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) # * nq_points * nq_points + data_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) nnz_a = 0 for i in range(v_n_inter): - for k in range(nq_points): - v_point = v_point_val[:, i * nq_points + k] - vd_point = vd_point_val[:, i * nq_points + k] - j_idx = v_point_idx[i * nq_points + k] - for l in range(u_n_inter): + j_idx = v_idx[i] # * nq_points + k + for l in range(u_n_inter): + i_idx = u_idx[l] # * nq_points + m + for k in range(nq_points): + v_point = v_val[:, i * nq_points + k] + vd_point = vd_val[:, i * nq_points + k] for m in range(nq_points): - u_point = u_point_val[:, l * nq_points + m] - ud_point = ud_point_val[:, l * nq_points + m] - i_idx = u_point_idx[l * nq_points + m] + u_point = u_val[:, l * nq_points + m] + ud_point = ud_val[:, l * nq_points + m] + for n in range(0, 3): # Hard-coded Kronecker product: vd = numpy.kron(vd_point, u_point) - data[3 * n:3 * (n + 1)] = vd_point[n] * u_point + v_diff_u[3 * n:3 * (n + 1)] = vd_point[n] * u_point # Hard-coded Kronecker product: ud = numpy.kron(v_point, ud_point) - data2[3 * n:3 * (n + 1)] = v_point[n] * ud_point + v_u_diff[3 * n:3 * (n + 1)] = v_point[n] * ud_point # column indices for data & data2 for p in range(0, 3): colv[3 * n + p] = (j_idx + n) * u_n_basf + i_idx + p @@ -430,10 +431,11 @@ def build_sparse_reg_matrix(self): jac = 1.0 / u_n_inter / v_n_inter coef = self._weights[m] * self._weights[k] * jac for n in range(0, 9): - row_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv - col_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv[n] - data_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = coef * (data[n] * data + data2[n] * data2) - nnz_a += n_uv_loc_nz * n_uv_loc_nz + data_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = coef * (v_diff_u[n] * v_diff_u + v_u_diff[n] * v_u_diff) + for n in range(0, 9): + row_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv + col_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv[n] + nnz_a += n_uv_loc_nz * n_uv_loc_nz mat_a = scipy.sparse.coo_matrix((data_m, (row_m, col_m)), shape=(u_n_basf * v_n_basf, u_n_basf * v_n_basf)).tocsr() From dc055972de76c3637776e227ebac33f744ffa7fe Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Wed, 27 Sep 2017 23:17:27 +0200 Subject: [PATCH 34/36] Hints for optimization --- src/bspline_approx.py | 16 +++++++++++++++- tests/test_bs_approx.py | 5 +++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index 777ba65..bac5372 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -401,7 +401,7 @@ def build_sparse_reg_matrix(self): data_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) nnz_a = 0 - + print("vnint: {} unint: {} nqp: {} prod: {}".format(v_n_inter, u_n_inter, nq_points, v_n_inter* u_n_inter* nq_points*nq_points)) for i in range(v_n_inter): j_idx = v_idx[i] # * nq_points + k for l in range(u_n_inter): @@ -413,6 +413,19 @@ def build_sparse_reg_matrix(self): u_point = u_val[:, l * nq_points + m] ud_point = ud_val[:, l * nq_points + m] + + # 9x9 dense matrix for single patch is: + # N'_i(u) * N'_k(u) * N_j(v)*N_l(v) + N_i(u) * N_k(u) * N'_j(v) * N'_l(v) + # Precomputing M'_ik(u) = N'_i(u) * N'_k(u) and M_ik(u) = N_i(u) * N_k(u) + # and similarly M'_jl(v) and M_jl(v) for u and v quad points we can compute + # whole local matrix as M_I(u_i) * M'_J(v_j) + M'_I(u_i)*M_J(v_j) = + # [ M_I, M'_I ](u_i) .dot. [ M'_J, M_J](v_j) + # sum_i,j X_I(u_i) .dot. Y_J(v_j) = + # (sum_i X_I(u_i)) .dot. (sum_j Y_J(v_j)) + # = A_I .dot. B_J + # A_I = sum_i [M_I, M'_I](u_i) + # B_J = sum_j [M'_J, M'_J](v_j) + # matrix shapes: (9,2) .dot. (2,9) for n in range(0, 3): # Hard-coded Kronecker product: vd = numpy.kron(vd_point, u_point) v_diff_u[3 * n:3 * (n + 1)] = vd_point[n] * u_point @@ -437,6 +450,7 @@ def build_sparse_reg_matrix(self): col_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv[n] nnz_a += n_uv_loc_nz * n_uv_loc_nz + print("Assembled") mat_a = scipy.sparse.coo_matrix((data_m, (row_m, col_m)), shape=(u_n_basf * v_n_basf, u_n_basf * v_n_basf)).tocsr() diff --git a/tests/test_bs_approx.py b/tests/test_bs_approx.py index 42340d0..2db43d3 100644 --- a/tests/test_bs_approx.py +++ b/tests/test_bs_approx.py @@ -90,7 +90,7 @@ def test_approx_func(self): xyz_func = eval_func_on_grid(function_sin_cos, xy_mat[:2,:2], xy_mat[:, 2], z_mat) print("Compare: Func - GridSurface.transform") - points = bs.make_function_grid(function_sin_cos, 30, 40) + points = bs.make_function_grid(function_sin_cos, 200, 200) gs = bs.GridSurface(points.reshape(-1, 3)) gs.transform(xy_mat, z_mat) xyz_grid = eval_z_surface_on_grid(gs, xy_mat[:2,:2], xy_mat[:, 2]) @@ -103,7 +103,8 @@ def test_approx_func(self): grid_cmp(xyz_func, xyz_grid, 0.02) print("Compare: Func - GridSurface.transform.Z_surf_approx") - z_surf = bs_approx.surface_from_grid(gs, (16, 24)) + #z_surf = bs_approx.surface_from_grid(gs, (16, 24)) + z_surf = bs_approx.surface_from_grid(gs, (100, 100)) #z_surf.transform(xy_mat, z_mat) xyz_grid = eval_z_surface_on_grid(z_surf, xy_mat[:2,:2], xy_mat[:, 2]) grid_cmp(xyz_func, xyz_grid, 0.02) From 2509dac2ee4f947ea75b9be06ecba62e2134affa Mon Sep 17 00:00:00 2001 From: jiri Date: Wed, 4 Oct 2017 08:45:21 +0200 Subject: [PATCH 35/36] construction of the matrix A was completely rewritten due to memory and performance issues --- src/bspline_approx.py | 97 +++++++++++-------------------------------- 1 file changed, 25 insertions(+), 72 deletions(-) diff --git a/src/bspline_approx.py b/src/bspline_approx.py index bac5372..c9cc70c 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -334,11 +334,11 @@ def build_ls_matrix(self): def _basis_in_q_points(self, basis): n_int = basis.n_intervals nq_points = len(self._q_points) - point_val = np.zeros((3, n_int * nq_points)) - d_point_val = np.zeros((3, n_int * nq_points)) - point_idx = np.zeros((n_int, 1)) q_point = np.zeros((n_int * nq_points, 1)) + point_val_outer = np.zeros((3, 3 * n_int)) # "3" considers degree 2 + d_point_val_outer = np.zeros((3, 3 * n_int)) # "3" considers degree 2 + #TODO: use numpy functions for quadrature points n = 0 for i in range(n_int): us = basis.knots[i + 2] @@ -346,15 +346,14 @@ def _basis_in_q_points(self, basis): for j in range(nq_points): up = us + uil * self._q_points[j] q_point[n] = up - # TODO: no need to find interval, idx = i - idx = basis.find_knot_interval(up) - u_base_vec = basis.eval_base_vector(idx, up) - u_base_vec_diff = basis.eval_diff_base_vector(idx, up) - point_val[:, n] = u_base_vec - d_point_val[:, n] = u_base_vec_diff + u_base_vec = basis.eval_base_vector(i, up) + u_base_vec_diff = basis.eval_diff_base_vector(i, up) + point_val_outer[:, 3 * i : 3 * (i + 1)] += self._weights[j] * np.outer(u_base_vec,u_base_vec) + d_point_val_outer[:, 3 * i : 3 * (i + 1)] += self._weights[j] * np.outer(u_base_vec_diff,u_base_vec_diff) n += 1 - point_idx[i] = idx - return point_val, d_point_val, point_idx, q_point + + return point_val_outer, d_point_val_outer,q_point + def build_sparse_reg_matrix(self): """ @@ -385,75 +384,29 @@ def build_sparse_reg_matrix(self): self._weights = [1.0 / 6, 5.0 / 6, 5.0 / 6, 1.0 / 6] nq_points = len(self._q_points) - # DONE: rename: u_vals, u_diffs, u_idxs, u_points - u_val, ud_val, u_idx, q_u_point = self._basis_in_q_points(self.u_basis) - v_val, vd_val, v_idx, q_v_point = self._basis_in_q_points(self.v_basis) - - # Matrix construction - # DONE: Assembly local dense blocks 9*9 and then put these nonzeroes into sparse matirx - # TODO: use numpy opperations to make assembly of local blocks readable, eliminate loops - colv = np.zeros(n_uv_loc_nz) - # DONE:rename data and data2 to something meqningful - v_diff_u = np.zeros(n_uv_loc_nz) - v_u_diff = np.zeros(n_uv_loc_nz) + u_val_outer, u_diff_val_outer, q_u_point = self._basis_in_q_points(self.u_basis) + v_val_outer, v_diff_val_outer, q_v_point = self._basis_in_q_points(self.v_basis) + row_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) - col_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) # * nq_points * nq_points + col_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) data_m = np.zeros((v_n_inter * u_n_inter * n_uv_loc_nz * n_uv_loc_nz)) + nnz_a = 0 + linsp = np.linspace(0,self.u_basis.degree,self.u_basis.degree+1) + llinsp = np.tile(linsp,self.u_basis.degree+1) print("vnint: {} unint: {} nqp: {} prod: {}".format(v_n_inter, u_n_inter, nq_points, v_n_inter* u_n_inter* nq_points*nq_points)) for i in range(v_n_inter): - j_idx = v_idx[i] # * nq_points + k for l in range(u_n_inter): - i_idx = u_idx[l] # * nq_points + m - for k in range(nq_points): - v_point = v_val[:, i * nq_points + k] - vd_point = vd_val[:, i * nq_points + k] - for m in range(nq_points): - u_point = u_val[:, l * nq_points + m] - ud_point = ud_val[:, l * nq_points + m] - - - # 9x9 dense matrix for single patch is: - # N'_i(u) * N'_k(u) * N_j(v)*N_l(v) + N_i(u) * N_k(u) * N'_j(v) * N'_l(v) - # Precomputing M'_ik(u) = N'_i(u) * N'_k(u) and M_ik(u) = N_i(u) * N_k(u) - # and similarly M'_jl(v) and M_jl(v) for u and v quad points we can compute - # whole local matrix as M_I(u_i) * M'_J(v_j) + M'_I(u_i)*M_J(v_j) = - # [ M_I, M'_I ](u_i) .dot. [ M'_J, M_J](v_j) - # sum_i,j X_I(u_i) .dot. Y_J(v_j) = - # (sum_i X_I(u_i)) .dot. (sum_j Y_J(v_j)) - # = A_I .dot. B_J - # A_I = sum_i [M_I, M'_I](u_i) - # B_J = sum_j [M'_J, M'_J](v_j) - # matrix shapes: (9,2) .dot. (2,9) - for n in range(0, 3): - # Hard-coded Kronecker product: vd = numpy.kron(vd_point, u_point) - v_diff_u[3 * n:3 * (n + 1)] = vd_point[n] * u_point - # Hard-coded Kronecker product: ud = numpy.kron(v_point, ud_point) - v_u_diff[3 * n:3 * (n + 1)] = v_point[n] * ud_point - # column indices for data & data2 - for p in range(0, 3): - colv[3 * n + p] = (j_idx + n) * u_n_basf + i_idx + p - - # Hard-coded Outer product: - # Jacobian * weights[m] * weights[k] * (numpy.outer(ud, ud) + numpy.outer(vd, vd)) - #u_q = q_u_point[l * nq_points + m, 0] - #v_q = q_v_point[i * nq_points + k, 0] - - # jacobian for UV coordinates should be used - jac = 1.0 / u_n_inter / v_n_inter - coef = self._weights[m] * self._weights[k] * jac - for n in range(0, 9): - data_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = coef * (v_diff_u[n] * v_diff_u + v_u_diff[n] * v_u_diff) - for n in range(0, 9): - row_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv - col_m[nnz_a + 9 * n:nnz_a + 9 * (n + 1)] = colv[n] + jac = 1.0 / u_n_inter / v_n_inter + coef = jac + data_m[nnz_a:nnz_a + n_uv_loc_nz * n_uv_loc_nz] = coef * ( np.kron(v_val_outer[:, 3 * i: 3 * (i + 1)],u_diff_val_outer[:, 3 * l: 3 * (l + 1)]).ravel() + + np.kron(v_diff_val_outer[:, 3 * i: 3 * (i + 1)],u_val_outer[:, 3 * l: 3 * (l + 1)]).ravel() ) + colv = np.repeat((i + linsp) * u_n_basf,self.u_basis.degree+1) + llinsp + col_m[nnz_a:nnz_a + n_uv_loc_nz * n_uv_loc_nz] = np.repeat(colv,n_uv_loc_nz) + row_m[nnz_a:nnz_a + n_uv_loc_nz * n_uv_loc_nz] = np.tile(colv,n_uv_loc_nz) nnz_a += n_uv_loc_nz * n_uv_loc_nz - print("Assembled") mat_a = scipy.sparse.coo_matrix((data_m, (row_m, col_m)), shape=(u_n_basf * v_n_basf, u_n_basf * v_n_basf)).tocsr() - - return mat_a - - + return mat_a \ No newline at end of file From ca1570fc6c92ddca42d3f8011453b7a9c74cc336 Mon Sep 17 00:00:00 2001 From: Jan Brezina Date: Mon, 9 Oct 2017 10:07:05 +0200 Subject: [PATCH 36/36] Enhance readability --- src/bspline.py | 7 ++++--- src/bspline_approx.py | 24 ++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/bspline.py b/src/bspline.py index b2377a3..9ad2166 100644 --- a/src/bspline.py +++ b/src/bspline.py @@ -121,8 +121,9 @@ def check(self, t, rtol = 1e-10): return t else: tol = (np.abs(t) + 1e-4)*rtol - assert self.domain[0] - tol <= t <= self.domain[1] + tol, \ - "Evaluate spline, t={}, out of domain: {}.".format(t, self.domain) + if not self.domain[0] - tol <= t <= self.domain[1] + tol: + raise IndexError("Evaluate spline, t={}, out of domain: {}.".format(t, self.domain)) + if t < self.domain[0]: return self.domain[0] else: @@ -813,7 +814,7 @@ def _get_grid_corners(self, xy_points): not la.norm(diff[1] + diff[3]) < self.step_tolerance * la.norm(diff[1]): raise GridNotInShapeExc("Grid XY envelope is not a parallelogram.") - self._point_tol = np.max( la.norm(diff, axis = 1) ) + self._point_tol = self.tolerance * np.max( la.norm(diff, axis = 1) ) self._u_step = diff[1] / (nu-1) # v10 - v00 self._v_step = diff[2] / (nv-1) # v11 - v10 diff --git a/src/bspline_approx.py b/src/bspline_approx.py index c9cc70c..2197212 100644 --- a/src/bspline_approx.py +++ b/src/bspline_approx.py @@ -335,8 +335,8 @@ def _basis_in_q_points(self, basis): n_int = basis.n_intervals nq_points = len(self._q_points) q_point = np.zeros((n_int * nq_points, 1)) - point_val_outer = np.zeros((3, 3 * n_int)) # "3" considers degree 2 - d_point_val_outer = np.zeros((3, 3 * n_int)) # "3" considers degree 2 + point_val_outer = np.zeros((3, 3, n_int)) # "3" considers degree 2 + d_point_val_outer = np.zeros((3, 3, n_int)) # "3" considers degree 2 #TODO: use numpy functions for quadrature points n = 0 @@ -348,8 +348,8 @@ def _basis_in_q_points(self, basis): q_point[n] = up u_base_vec = basis.eval_base_vector(i, up) u_base_vec_diff = basis.eval_diff_base_vector(i, up) - point_val_outer[:, 3 * i : 3 * (i + 1)] += self._weights[j] * np.outer(u_base_vec,u_base_vec) - d_point_val_outer[:, 3 * i : 3 * (i + 1)] += self._weights[j] * np.outer(u_base_vec_diff,u_base_vec_diff) + point_val_outer[:, :, i] += self._weights[j] * np.outer(u_base_vec,u_base_vec) + d_point_val_outer[:, :, i] += self._weights[j] * np.outer(u_base_vec_diff,u_base_vec_diff) n += 1 return point_val_outer, d_point_val_outer,q_point @@ -399,13 +399,17 @@ def build_sparse_reg_matrix(self): for i in range(v_n_inter): for l in range(u_n_inter): jac = 1.0 / u_n_inter / v_n_inter - coef = jac - data_m[nnz_a:nnz_a + n_uv_loc_nz * n_uv_loc_nz] = coef * ( np.kron(v_val_outer[:, 3 * i: 3 * (i + 1)],u_diff_val_outer[:, 3 * l: 3 * (l + 1)]).ravel() - + np.kron(v_diff_val_outer[:, 3 * i: 3 * (i + 1)],u_val_outer[:, 3 * l: 3 * (l + 1)]).ravel() ) + idx_range = n_uv_loc_nz * n_uv_loc_nz + v_val_outer_loc = v_val_outer[:, :, i] + dv_val_outer_loc = v_diff_val_outer[:, : , i] + u_val_outer_loc = u_val_outer[:, :, i] + du_val_outer_loc = u_diff_val_outer[:, : , i] + data_m[nnz_a:nnz_a + idx_range] = jac * ( np.kron(v_val_outer_loc, du_val_outer_loc) + + np.kron(dv_val_outer_loc, u_val_outer_loc) ).ravel() colv = np.repeat((i + linsp) * u_n_basf,self.u_basis.degree+1) + llinsp - col_m[nnz_a:nnz_a + n_uv_loc_nz * n_uv_loc_nz] = np.repeat(colv,n_uv_loc_nz) - row_m[nnz_a:nnz_a + n_uv_loc_nz * n_uv_loc_nz] = np.tile(colv,n_uv_loc_nz) - nnz_a += n_uv_loc_nz * n_uv_loc_nz + col_m[nnz_a:nnz_a + idx_range] = np.repeat(colv,n_uv_loc_nz) + row_m[nnz_a:nnz_a + idx_range] = np.tile(colv,n_uv_loc_nz) + nnz_a += idx_range print("Assembled") mat_a = scipy.sparse.coo_matrix((data_m, (row_m, col_m)), shape=(u_n_basf * v_n_basf, u_n_basf * v_n_basf)).tocsr()