From 31f919a50f6cd370f6b79ab738e20ff50cae71fb Mon Sep 17 00:00:00 2001 From: Robin Roy <115863770+robinroy03@users.noreply.github.com> Date: Fri, 7 Jun 2024 15:38:06 +0530 Subject: [PATCH 01/11] fix: gitignore (#893) * fix: gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1f8a10c9c..9c8b8e457 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ docs/source/api docs/source/auto_examples docs/source/auto_tutorials docs/source/reference +docs/source/sg_execution_times.rst docs/examples/*.png docs/examples/*.vtk docs/examples/*.gif From b38afc1676b8069a4eb4f34c01d5545029723546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wachiou=20BOURA=C3=8FMA?= <100234404+WassCodeur@users.noreply.github.com> Date: Mon, 10 Jun 2024 09:20:21 +0000 Subject: [PATCH 02/11] DOC: Wachiou Week 1 Blog Post (#894) * DOC: Wachiou Week 1 Blog Post FIX: remove trailing-whitespace * RF: Update acknowledgements and week number in post --- .../2024-06-06-week1-wachiou-bouraima.rst | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/source/posts/2024/2024-06-06-week1-wachiou-bouraima.rst diff --git a/docs/source/posts/2024/2024-06-06-week1-wachiou-bouraima.rst b/docs/source/posts/2024/2024-06-06-week1-wachiou-bouraima.rst new file mode 100644 index 000000000..b16a3f960 --- /dev/null +++ b/docs/source/posts/2024/2024-06-06-week1-wachiou-bouraima.rst @@ -0,0 +1,61 @@ +WEEK 1: Progress and challenges at Google Summer of Code (GSoC) 2024 +==================================================================== + +.. post:: June 06, 2024 + :author: Wachiou BOURAIMA + :tags: google + :category: gsoc + +Hello👋🏾, + +Welcome back to my Google Summer of Code (GSoC) 2024 journey! +This week has been filled with progress and challenges as I continue to work on modernizing the FURY code base. + + +Applying the keyword_only decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +My main task this week was to apply the keyword_only decorator to several functions. +The decorator ensures that all arguments except the first are keyword-only, +which helps to make the code clearer and parameter passing more explicit. +Some warnings appeared after applying this decorator, and to resolve them, +I updated all the code where these functions were called with the necessary format. This was a very important step in maintaining the integrity and functionality of the code base. + + +Managing the challenges of Git rebasing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Rebasing the branch I was working on was the other major activity of my week. +It was a real challenge because of the conflicts that arose and had to be resolved. +It involved a lot of research and problem-solving on how to resolve these conflicts, +which greatly enhanced my understanding of Git. It was a challenging but satisfying experience of version control management and complex mergers. + + +Peer code review +~~~~~~~~~~~~~~~~ + +In addition to my duties, I was also tasked with reviewing the code of my peers. +This exercise was very rewarding, as it enabled me to understand different coding styles and approaches. +The constructive comments and suggestions were beneficial not only for teammates, +but also for improving my own coding and reviewing skills. + + +Acknowledgements +~~~~~~~~~~~~~~~~~ + +I would like to thank all my classmates: `Iñigo Tellaetxe Elorriaga `_, `Robin Roy `_, `Kaustav Deka `_ and my guide: `Serge Koudoro `_ for their constructive suggestions on my work. +Their ideas and suggestions were of great help to me and I am grateful for their support and advice. + + +What happens next? +~~~~~~~~~~~~~~~~~~ + +Here's a summary of what I plan to do in week two: + +- Apply the keyword_only decorator to all other necessary functions. +- Update the calling of these functions in the code to ensure consistency and avoid raising warnings. +- Rename the decorator with a more descriptive name. +- Add two parameters to the decorator, specifying from which version of FURY it will work. + + +🥰Thanks for reading! Your comments are most welcome, and I look forward to giving you a sneak preview of my work next week. From 32799d23ee0960f02dde5d6bd285b54108f35c40 Mon Sep 17 00:00:00 2001 From: Robin Roy <115863770+robinroy03@users.noreply.github.com> Date: Thu, 13 Jun 2024 19:26:24 +0530 Subject: [PATCH 03/11] [DOC] week 1 blog GSoC (#892) * [GSoC] week 1 - robin --- .../posts/2024/2024-06-06-week-1-robin.rst | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/source/posts/2024/2024-06-06-week-1-robin.rst diff --git a/docs/source/posts/2024/2024-06-06-week-1-robin.rst b/docs/source/posts/2024/2024-06-06-week-1-robin.rst new file mode 100644 index 000000000..c6b5a8495 --- /dev/null +++ b/docs/source/posts/2024/2024-06-06-week-1-robin.rst @@ -0,0 +1,115 @@ +Week 1: It officially begins... +=============================== + +.. post:: June 06 2024 + :author: Robin Roy + :tags: google + :category: gsoc + +Hi, I'm `Robin `_ and this is my blog about week 1. + +My goal for week 1 was to start with `Retrieval-Augmented Generation (RAG) `_, check different databases and host every endpoint. My week1 and week2 are very intertwined because I applied everything I did during week1 on week2. (I'm writing this blog midway through week2) + +why phi-3-mini-4k-instruct? +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before I detail everything I've done this week, I'll explain why `phi-3 mini 4k `_ was chosen as the LLM, I forgot to mention this in the last blog. Phi-3 is a small 3.8B 4k context model, it means it can work with 4k tokens(similar to words) at a time. Due to its small size, it runs fast both locally and on Huggingface. Performance wise comparatively with other opensource models, it performs decently well. In the `LMSYS LLM leaderboard `_ phi-3 mini 4k comes with an ELO of 1066 (59th position). But it achieves this as a small model. +I also tried Llama3-8B, it performs better than phi-3 mini with an ELO of 1153 and rank 22. But it is considerably slower for inference. Due to this, I chose phi-3 mini for now. + + +Things I did week-1 (and some week2) +------------------------------------ + +1) **Choosing the vector database** + +I decided to choose `Pinecone `_ as the vector DB because it had a very generous free tier. Other options on consideration were `pgvector `_ and `chromadb `_, but they didn't have a free tier. + +2) **PR Submissions and Review** + +I also merged a `PR `_ on FURY which fixes a CI issue. I also spent time doing review of other PRs from my fellow GSoC mates. + +3) **Deciding which embedding model to use** + +A good embedding model is necessary to generate embeddings which we then upsert into the DB. Ollama had embedding model support, but I found the catalogue very small and the models they provided were not powerful enough. Therefore I decided to try using HuggingFace Sentence Transformers. +Sentence Transformers have a very vibrant catalogue of models available of various sizes. I chose `gte-large-en-v1.5 `_ from Alibaba-NLP, an 8k context, 434 million parameter model. It only had a modest memory requirement of 1.62 GB. +Performance wise, it ranks 11th on the `MTEB leaderboard `_. It is a very interesting model due to its size:performance ratio. + +4) **Hosting the embedding model** + +Hosting this sentence-transformer model was confusing. For some reason, the HF spaces were blocking the Python script from writing on ``.cache`` folder. Docker container inside spaces runs with user id 1000 (non-root user), therefore I had to give it permission to download and store files. + +I've hosted 5 gunicorn workers to serve 5 parallel requests at a time. Since the model is small, this is possible. + +5) **Hosting the database endpoint** + +I wrapped the pinecone DB API into an endpoint so it'll be easy to query and receive the results. +It is also configured to accept 5 concurrent requests although I could increase it a lot more. + +I upserted docstrings from ``fury/actor.py`` into the vector DB for testing. So now, whenever you ask a question the model will use some ``actor.py`` function to give you an answer. For now, it could be used like a semantic function search engine. + +I decided to abstract the DB endpoint to reduce the dependency on one provider. We can swap the providers as required and keep all other features running. + +6) **Hosting Discord Bot** + +So with this, all the endpoints are finally online. The bot has some issues, it is going offline midway for some reason. I'll have to see why that happens. + +For some reason, Huggingface spaces decided to not start the bot script. Later a community admin from Huggingface told me to use their official bot implementation as a reference. This is why I had to use threading and gradio to get the bot running (migrating to docker can be done, but this is how they did it and I just took that for now). + +Huggingface spaces need a script to satisfy certain criteria to allow them to run, one of them is a non-blocking I/O on the main loop. So I had to move the discord bot to a new thread. + +Connecting all of them together! +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +So now we have 4 hosted services, all hosted on HuggingFace spaces: + - Discord Bot + - LLM API + - Embeddings API + - Database API + +Now we'll have to connect them all to get an answer to the user query. + +This is the current architecture, there's a lot of room for improvement here. + + + .. raw:: html + + + +The Language Model takes the context and the user query, combines them to form an answer and returns to the user through discord (for now). Maybe moving the core logic from discord bot to a separate node might be good, and connect discord/github/X to that node. +The database takes embeddings and do an Approximate Nearest Neighbor search (a variant of KNN) and returns top-k results (k=3 for now). + + .. raw:: html + + + +What is coming up next week? +---------------------------- + +Answer quality improvements. Also, the discord bot dies randomly, have to fix that also. + +Did you get stuck anywhere? +--------------------------- + +Was stuck in hosting models on Huggingface spaces, fixed it later. + +LINKS: + +- `Discord Bot `_ + +- `Database Repo `_ + +- `Embedding Repo `_ + +- `LLM Repo `_ + +- `Retrieval-Augmented Generation (RAG) `_ +- `phi-3 mini 4k `_ +- `LMSYS LLM leaderboard `_ +- `Pinecone `_ +- `pgvector `_ +- `chromadb `_ +- `PR `_ +- `gte-large-en-v1.5 `_ +- `MTEB leaderboard `_ + +Thank you for reading! From 06d2cd920bd19c17c643800604e8e3246188801b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wachiou=20BOURA=C3=8FMA?= <100234404+WassCodeur@users.noreply.github.com> Date: Mon, 24 Jun 2024 21:13:48 +0000 Subject: [PATCH 04/11] DOC: Add my week 2 blog post (#897) FIX: remove trailing-whitespace DOC: function names warn_on_args_to_kwargs and keyword_only as inline code --- .../2024-06-15-week2-wachiou-bouraima.rst | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/source/posts/2024/2024-06-15-week2-wachiou-bouraima.rst diff --git a/docs/source/posts/2024/2024-06-15-week2-wachiou-bouraima.rst b/docs/source/posts/2024/2024-06-15-week2-wachiou-bouraima.rst new file mode 100644 index 000000000..fd87a11c2 --- /dev/null +++ b/docs/source/posts/2024/2024-06-15-week2-wachiou-bouraima.rst @@ -0,0 +1,52 @@ +WEEK 2: Refinements and Further Enhancements +============================================ + +.. post:: June 15, 2024 + :author: Wachiou BOURAIMA + :tags: google + :category: gsoc + +Hello again, +~~~~~~~~~~~~~ + +Welcome back to my Google Summer of Code (GSoC) 2024 journey! This week has been dedicated to refining and improving the work done so far, with a particular focus on the keyword_only decorator. + + +Renaming and Updating the Decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This week, I've updated `this Pull Request `_ by renaming the ``keyword_only`` decorator to ``warn_on_args_to_kwargs`` for greater clarity. The updated decorator now includes version parameters from_version and until_version. This enhancement ensures that the decorator will raise a RuntimeError if the current version of FURY is greater than until_version. + + +Peer Code Review +~~~~~~~~~~~~~~~~~ + +I also spent time reviewing `Kaustav Deka's `_ code. This exercise remains rewarding, as it helps me understand different coding styles and approaches. Constructive feedback and suggestions from my classmates were invaluable, not only in helping my teammates but also in improving my own coding and reviewing skills. + + +Research into lazy loading +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In parallel, I started researching the lazy loading feature and thinking about how to implement it. This feature will optimize performance by loading resources only when they're needed, which is crucial to improving the efficiency of FURY's code base. + + +Acknowledgements +~~~~~~~~~~~~~~~~ + +I am deeply grateful to my classmates `Iñigo Tellaetxe Elorriaga `_, `Robin Roy `_, and `Kaustav Deka `_ for their insightful suggestions and comments on my work. +Special thanks to my mentor, `Serge Koudoro `_, whose guidance and support enabled me to meet the challenges of this project. +Their combined efforts have greatly contributed to my progress, and I appreciate their continued help. + + +What happens next? +~~~~~~~~~~~~~~~~~~ + +For week 3, I plan to : + +- Ensure that the ``warn_on_args_to_kwargs`` decorator is applied consistently in all necessary functions. +- Continue to update the calling of these functions in the code to maintain consistency and avoid warnings. +- Refine decorator as necessary based on feedback and testing. +- Start implementing lazy loading functionality based on my research to optimize performance. + + +🥰 Thank you for taking the time to follow my progress. Your feedback is always welcome and I look forward to sharing more updates with you next week. From 300b801ec638f739faf43ae247329b8330db0d7d Mon Sep 17 00:00:00 2001 From: Robin Roy <115863770+robinroy03@users.noreply.github.com> Date: Tue, 25 Jun 2024 03:01:12 +0530 Subject: [PATCH 05/11] DOC: GSoC Week 2 & 3 (#896) * GSoC: Week 2 and 3, Robin * GSoC week3 robin - added video link * fix: codespell --- ...obin-3-fury-discord-bot-references-url.jpg | Bin 0 -> 61352 bytes .../posts/2024/2024-06-16-week2-robin.rst | 79 +++++++++++++++++ .../posts/2024/2024-06-16-week3-robin.rst | 82 ++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 docs/source/_static/images/gsoc-robin-3-fury-discord-bot-references-url.jpg create mode 100644 docs/source/posts/2024/2024-06-16-week2-robin.rst create mode 100644 docs/source/posts/2024/2024-06-16-week3-robin.rst diff --git a/docs/source/_static/images/gsoc-robin-3-fury-discord-bot-references-url.jpg b/docs/source/_static/images/gsoc-robin-3-fury-discord-bot-references-url.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d9bdec1f47fcf05d410dad6ab1ecf6cf0861395c GIT binary patch literal 61352 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 KSuXL_?|%WD3{Um| literal 0 HcmV?d00001 diff --git a/docs/source/posts/2024/2024-06-16-week2-robin.rst b/docs/source/posts/2024/2024-06-16-week2-robin.rst new file mode 100644 index 000000000..cfcb5235b --- /dev/null +++ b/docs/source/posts/2024/2024-06-16-week2-robin.rst @@ -0,0 +1,79 @@ +Week 2: The first iteration! +============================ + +.. post:: June 16 2024 + :author: Robin Roy + :tags: google + :category: gsoc + +Hi, I'm `Robin `_ and this is my blog about week 2. + +My goal for week 2 was to connect everything and make a prototype. So now we have a bot working 24x7 to answer all your doubts :) + +Apart from the things mentioned in my `week 1 blog `_, the things I did in week 2 are basically: + - Chunking the files for embedding. + - Upserting the chunks into the database. + - Connecting everything together. + - Making the discord bot async. + - Merging a PR. + +1) **Chunking the files for embedding** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the context of building LLM-related applications, chunking is the process of breaking down large pieces of text into smaller segments. It's an essential technique that helps optimize the relevance of the content we get back from a vector database once we use an embedding model to embed content. For our case with FURY, our data is entirely code. So one approach I tried was to take docstrings and the function/class signature. + +I used a naive parser during week 2, which used a combination of regex and common pattern matching to do this splitting. Later my mentors `Mohamed `_ and `Serge `_ told me to use a better approach, using the python ``inspect`` module. + +Another thing to consider was the chunk size. It is shown that smaller chunks outperform larger chunks. This can be intuitively thought of like this: An embedding model can compress a smaller text to 1024 vectors without much data loss compared to compressing a larger text to 1024 vectors. + +This also introduces another important issue, we need a way to test it based on our model. So we need benchmarking. + + +2) **Upserting chunks into the database** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +I upserted all the chunks into the database, along with the vectors I gave metadata which was the function signature and docstrings. Later in week 3, we'll modify this to show links instead of the big wall of text. + + +3) **Connecting everything together** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +I took the 4 key parts - Discord Bot, LLM API, Embeddings API and the Database API and connected them together. This was explained on the `week 1 blog `_ itself. + + +4) **Making the Discord Bot async** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One of the biggest challenges I faced this week was to get everything running properly. LLM output takes a lot of time to generate (we'll fix this amazingly well in week 3 BTW). +I made a big mistake, I used ``requests`` library to do the REST API calls. It occurred to me later that it is synchronous and does blocking calls. This was the reason my Discord bot was dying randomly. I fixed it by migrating to ``aiohttp``. + +This also made me realize I can use async in a lot of other places. A lot of these tasks are I/O bound. If I make them async we might be able to take many more concurrent requests. + +5) **Merging a PR** +~~~~~~~~~~~~~~~~~~~ + +I merged a `PR `_ which modifies `.gitignore`. I found this while generating the Sphinx docs. + + +What is coming up next week? +---------------------------- + +- A faster LLM inference. +- Better pipeline for data collection. +- Links for citation. + +Did you get stuck anywhere? +--------------------------- + +Took me some time to realize I was using synchronous code inside async. Fixed it later. + + +LINKS: + +- `Week 1 Blog `_ +- `PR `_ +- `Serge Koudoro `_ +- `Mohamed Abouagour `_ +- `Robin :) `_ + +Thank you for reading! diff --git a/docs/source/posts/2024/2024-06-16-week3-robin.rst b/docs/source/posts/2024/2024-06-16-week3-robin.rst new file mode 100644 index 000000000..ba6c2e2e9 --- /dev/null +++ b/docs/source/posts/2024/2024-06-16-week3-robin.rst @@ -0,0 +1,82 @@ +Week 3: Data Data Data! +======================= + +.. post:: June 16 2024 + :author: Robin Roy + :tags: google + :category: gsoc + +Hi, I'm `Robin `_ and this is my blog about week 3. + +My goal for week 3 was to collect data more efficiently and improve the citations. I also had my mid-terms during this week so I had to get things done fast. + +Things I did in week 3 +---------------------- + +1) **A better data parsing technique** + +My initial approach was naive, it was just regex and some common filtrations. Later, my mentors told me to use the `inspect` module. I studied that module and realized that I needed to parse data using an AST. I didn't use the `inspect` module to do the parsing, since I only had to get the function/class signature and docstrings. So instead I used the ``ast`` module from python stdlib. My mentors gave me the general direction to go through - which was using ASTs to parse data effectively. + +So now we have a script which you run like `python extractor.py fury` and it'll generate the appropriate JSON files. + +`{"path": "../..", "function/class name": "name", "docstring": "..", "class_methods": ["method1", "..."]}` + +I also changed the upserting chunk format. Earlier it was just strings, now it is JSON (same thing above). I do not have a scientific reason for this, but empirically it looks like it helped. Benchmarking is something I'm planning to do next week. + +Metadata format: + +`metadata: {"path": "../..", "function/class name": "name", "docstring": "..", "methods": [(method1, docstring), (method2, docstring), ...]}` + +2) **Links for citation** + +Now the bot shows links for citations. Because of the new parsing, I was able to do that pretty efficiently. + +.. image:: /_static/images/gsoc-robin-3-fury-discord-bot-references-url.jpg + :alt: Link based references for the LLM output. + + +3) **Faster Inference** + +So this is something about the Generative AI field. There are too many things happening you might miss some stuff. `Groq` is a company providing free APIs for the llama and other opensource models (free for now, at least). Its inference speed is also super high. So I decided to integrate that also into our infrastructure. +Since everything is a microservice in our architecture, it is easy to add new things. + +Our architecture: + .. raw:: html + + + +So now, along with Ollama, we have Groq inference also. I aim to make a `router` so that we can swap different providers as required. I'm also very interested in integrating Google Gemini 1.5 Flash and other models. Groq does not support fine-tuning, but Flash supports it and is `free of cost `_ (for now). Our architecture is platform agnostic, so we can try out different things without being locked into any of them. We will also fine-tune our phi3 model since we have the data with us. + + .. raw:: html + + + +4) **Dockerizing Discord Bot** + +I earlier used the huggingface implementation (copied their implementation demo). It was bad. My mentors suggested to dockerize the bot so I did that. + + +What is coming up next week? +---------------------------- + +- Benchmarking. Now we have the data, but we need to properly benchmark to see whether the modifications I make every day are making the bot dumber or smarter. +- Study different techniques to improve model answer accuracy such as `HyDE `_. +- Study how to go forward with fine-tuning. +- Improved references. +- Collect more data. + + +Did you get stuck anywhere? +--------------------------- + +No, everything went well this week. Exam preparation was a pain though😢. + +LINKS: + +- `Gemini Blog `_ + +- `HyDE `_ + +- `Robin :) `_ + +Thank you for reading! From 1975a3425813d9afe4aaf619f6df3e52db0eb44e Mon Sep 17 00:00:00 2001 From: Serge Koudoro Date: Tue, 25 Jun 2024 19:27:50 +0300 Subject: [PATCH 06/11] FIX: `ptp` was removed from the ndarray class in NumPy 2.0. Use np.ptp(arr, ...) instead. --- fury/colormap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fury/colormap.py b/fury/colormap.py index 932191227..4d9a5c0b1 100644 --- a/fury/colormap.py +++ b/fury/colormap.py @@ -669,7 +669,7 @@ def rgb2hsv(rgb): out_v = rgb.max(-1) # -- S channel - delta = rgb.ptp(-1) + delta = np.ptp(rgb, -1) # Ignore warning for zero divided by zero old_settings = np.seterr(invalid="ignore") out_s = delta / out_v From 1e07b8f9920ca1dfb72c74aa291f56fa3e231d96 Mon Sep 17 00:00:00 2001 From: Serge Koudoro Date: Tue, 25 Jun 2024 19:47:37 +0300 Subject: [PATCH 07/11] CI: remove 3.8 add 3.12 (#903) --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3bdb5fb8..07b9b3c95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, '3.10', 3.11] + python-version: [3.9, '3.10', 3.11, 3.12] os: [ubuntu-latest, macos-latest, windows-latest] platform: [x64] install-type: [pip, ] # conda] @@ -34,13 +34,13 @@ jobs: use-pre: [false] include: - os: macos-latest # ubuntu-latest - python-version: '3.10' + python-version: 3.11 install-type: pip depends: OPTIONAL_DEPS coverage: true use-pre: false - os: ubuntu-latest - python-version: '3.10' + python-version: 3.12 install-type: pip depends: OPTIONAL_DEPS coverage: false From 21d6932262fa08d2dd6db0b9215f7e4491de344d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wachiou=20BOURA=C3=8FMA?= <100234404+WassCodeur@users.noreply.github.com> Date: Wed, 10 Jul 2024 07:17:13 +0000 Subject: [PATCH 08/11] DOC: My weeks 3 and 4 blog post (#905) * DOC: Add Wachiou's Weeks 3 and 4 blogs post * FIX: completes the titles underline and corrects a grammatical error * FIX: Correct typos --- .../2024-06-26-week3-wachiou-bouraima.rst | 74 ++++++++++++ .../2024-06-26-week4-wachiou-bouraima.rst | 112 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 docs/source/posts/2024/2024-06-26-week3-wachiou-bouraima.rst create mode 100644 docs/source/posts/2024/2024-06-26-week4-wachiou-bouraima.rst diff --git a/docs/source/posts/2024/2024-06-26-week3-wachiou-bouraima.rst b/docs/source/posts/2024/2024-06-26-week3-wachiou-bouraima.rst new file mode 100644 index 000000000..cca107b68 --- /dev/null +++ b/docs/source/posts/2024/2024-06-26-week3-wachiou-bouraima.rst @@ -0,0 +1,74 @@ +WEEK 3: Refinements and Further Enhancements +============================================ + +.. post:: June 26, 2024 + :author: Wachiou BOURAIMA + :tags: google + :category: gsoc + +Hello everyone, +--------------- + +Welcome to the fourth week of my Google Summer of Code (GSoC) 2024 journey! +This week I've been delving into the technical aspects of my project, +focusing on the consistent application of the ``warn_on_args_to_kwargs`` decorator and the initial implementation of lazy loading. + + +Consistent application of ``warn_on_args_to_kwargs`` +---------------------------------------------------- + +This week I continued to apply the decorator to functions. +To ensure consistency across the code base, I audited all functions that could benefit from the ``warn_on_args_to_kwargs`` decorator. +To do this, I had to: + +1. Identify target functions: + + * Identify functions that could benefit from the decorator. + * continue reviewing the code base to identify functions that accept both positional and keyword arguments. + +2. Applying the Decorator: + + * For each identified function, I added the ``warn_on_args_to_kwargs`` decorator. + * Example: + +.. code-block:: python + + @warn_on_args_to_kwargs() + def get_actor_from_primitive( + vertices, + triangles, + *, + colors=None, + normals=None, + backface_culling=True, + prim_count=1, + ): + +3. Updating Unit Tests: + +* updated all the unit tests for the functions where the ``warn_on_args_to_kwargs`` decorator is applied to ensure they respect the new format. +* Example: + +.. code-block:: python + + actr = get_actor_from_primitive(big_verts, big_faces, colors=big_colors) + +- You can find more details and the implementation in my pull request: `https://github.com/fury-gl/fury/pull/888 `_. + + +What Happens Next? +------------------ + +For week 4, I plan to: + +* Continue refining the ``warn_on_args_to_kwargs`` decorator based on feedback from my Peers `Iñigo Tellaetxe Elorriaga `_, `Robin Roy `_, `Kaustav Deka `_, my guide: `Serge Koudoro `_ and the other community members. +* Apply the ``warn_on_args_to_kwargs`` decorator to all the remaining modules and update all the unit tests of these modules too, to respect the desired format. +* Dive deep into the lazy loading functionality based on my research to optimize performance. +* Further engage in code reviews to support my peers and improve our project. + +Did I get stuck? +---------------- + +I didn't get stuck. + +Thank you for following my progress. Your feedback is always welcome. diff --git a/docs/source/posts/2024/2024-06-26-week4-wachiou-bouraima.rst b/docs/source/posts/2024/2024-06-26-week4-wachiou-bouraima.rst new file mode 100644 index 000000000..2bb6def37 --- /dev/null +++ b/docs/source/posts/2024/2024-06-26-week4-wachiou-bouraima.rst @@ -0,0 +1,112 @@ +WEEK 4: Updating Decorator, Exploring Lazy Loading, and Code Reviews +==================================================================== + +.. post:: June 26, 2024 + :author: Wachiou BOURAIMA + :tags: google + :category: gsoc + +Hello everyone, +--------------- + +Welcome again to my Google summer of code 2024 (GSoC' 2024) journey 2024!. +This week, I focused on updating the ``warn_on_args_to_kwargs`` decorator, applying it across multiple modules, exploring lazy loading, and continuing with code reviews. + + +Updating the ``warn_on_args_to_kwargs`` decorator +------------------------------------------------- + +Based on feedback from my mentor `Serge Koudoro `_ and peers `Iñigo Tellaetxe Elorriaga `_, `Robin Roy `_, `Kaustav Deka `_, I refined the ``warn_on_args_to_kwargs`` decorator and its associated unit tests: + +1. Improvements: + + - Added conditions to verify if the values of ``from_version``, ``until_version``, and the current version of FURY are respected. This includes handling cases where ``from_version`` is greater than the current version of FURY, ``until_version`` is less than the current version of FURY, and ``until_version`` is greater than or equal to the current version of FURY. + - Ensured the decorator and tests cover a broader range of edge cases. + - Enhanced the warning messages for better clarity and guidance. + +2. Doctest Updates: + + - Updated the doctest considering the values of `from_version` and `until_version`. + - Moved the doctest from the `def decorator()` function to the root function. + +3. Unit Tests: + +.. code-block:: python + + def test_warn_on_args_to_kwargs(): + @warn_on_args_to_kwargs() + def func(a, b, *, c, d=4, e=5): + return a + b + c + d + e + + # if FURY_CURRENT_VERSION is less than from_version + fury.__version__ = "0.0.0" + npt.assert_equal(func(1, 2, 3, 4, 5), 15) + npt.assert_equal(func(1, 2, c=3, d=4, e=5), 15) + npt.assert_raises(TypeError, func, 1, 3) + +- This ensures robust validation and helps catch potential issues early. + + +Applying the ``warn_on_args_to_kwargs`` Decorator +----------------------------------------------- + +This week, I applied the ``warn_on_args_to_kwargs`` decorator to several modules, ensuring consistent usage and improved code quality. The modules updated include: + +- `actors` +- `ui` +- `animation` +- `shares` +- `data` + +For each module, I opened a pull request to track the changes and facilitate reviews: + +- `actors`: https://github.com/fury-gl/fury/pull/898 +- `animation`: https://github.com/fury-gl/fury/pull/899 +- `data`: https://github.com/fury-gl/fury/pull/900 +- `shares`: https://github.com/fury-gl/fury/pull/901 +- `ui`: https://github.com/fury-gl/fury/pull/902 + + +Exploring lazy loading +---------------------- + +In order to optimize performance, I've started exploring and implementing lazy loading. This week, the focus was on the following points: + +- Getting to grips with how the lazy loader works +- Implementing some small script to understand how the lazy loader works +- I also read the SPEC1 document available at `SPEC1 `_ +- Understanding the benefits of lazy loading and how it can be applied to the FURY code base +- Planning the integration of lazy loading into the FURY code base + +Code sample: ``_ + + +Peer Code Review +---------------- + +This week, I continued to dedicate time to reviewing the code of my peers. Specifically, I reviewed Kaustav Deka’s work, providing constructive feedback and suggestions for improvement. You can view the pull request here: `https://github.com/dipy/dipy/pull/3239 `_. + + +Acknowledgements +---------------- + +I am deeply grateful to my classmates `Iñigo Tellaetxe Elorriaga `_, `Robin Roy `_, `Kaustav Deka `_ for their continuous support and insightful suggestions. Special thanks to my mentor, `Serge Koudoro `_ , whose expertise and guidance have been invaluable in navigating these technical challenges. + + +Did I get stuck? +----------------- + +Yes, I was a bit confused about understanding lazy loader, but thanks to the help of my mentor `Serge Koudoro `_ , I was able to understand it better. + + +What's next? +------------ + +For the upcoming week, I plan to: + +- Implement lazy loading in the FURY code base +- Continue refining the ``warn_on_args_to_kwargs`` decorator based on feedback +- Engage in more code reviews to support my peers +- Prepare to working on the FURY website to improve the documentation and user experience + +Thank you for following my progress. Your feedback is always welcome. From 6bcadf98d0fc95676568d225d6c6e0e668224057 Mon Sep 17 00:00:00 2001 From: Robin Roy <115863770+robinroy03@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:06:40 +0530 Subject: [PATCH 09/11] [DOC] GSoC week 4 and 5 (#906) * [DOC] GSoC week 4 and 5 * typo fix * updated the description of rst & bugfixes * DOC: updates on json code formatting and being more explicit on colab to script (week4) --- .../_static/images/gsoc_llm_robin_week5.jpg | Bin 0 -> 56897 bytes .../posts/2024/2024-07-01-week-4-robin.rst | 74 ++++++++++ .../posts/2024/2024-07-01-week-5-robin.rst | 129 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 docs/source/_static/images/gsoc_llm_robin_week5.jpg create mode 100644 docs/source/posts/2024/2024-07-01-week-4-robin.rst create mode 100644 docs/source/posts/2024/2024-07-01-week-5-robin.rst diff --git a/docs/source/_static/images/gsoc_llm_robin_week5.jpg b/docs/source/_static/images/gsoc_llm_robin_week5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..71be430865c8228f5c43cb1d18c64691f60654cf GIT binary patch literal 56897 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 literal 0 HcmV?d00001 diff --git a/docs/source/posts/2024/2024-07-01-week-4-robin.rst b/docs/source/posts/2024/2024-07-01-week-4-robin.rst new file mode 100644 index 000000000..0172bff7b --- /dev/null +++ b/docs/source/posts/2024/2024-07-01-week-4-robin.rst @@ -0,0 +1,74 @@ +Week 4: Pipeline Improvements and Taking The Bot Public! +======================================================== + +.. post:: July 1 2024 + :author: Robin Roy + :tags: google + :category: gsoc + +Hi, I'm `Robin `_ and this is my blog about week 4. + +My goals for week 4 were to move my Google colab notes to a proper Python script, improve the existing code, and make a working pipeline to upsert data easily. Also, the bot is public now :) Anyone reading this blog could join this `Discord Server `_ and ask questions right away! + +Things I did in Week 4 +---------------------- + +1) **Chunking tutorials and documentation** + +Earlier, only files fitting the context window of the embedding model were upserted. This was because otherwise, we'd have to split the file in half and lose the overall context. This will lead to information loss and retrieval will be messy. Now, I decided I'd upsert everything by splitting information properly. By "properly", what I mean is it won't be a random split, and there'll be logical reasoning behind every chunk. + +This area is still actively studied, and the whole concept is to find ideal chunks which are self-sufficient and contain the most information. This `notebook `_ details 6 different approaches, I read through them and some of their associated literature and decided we'll use `Recursive Character Text Splitting` and `Document Specific Splitting` for now. There is no major reason for this, I just felt it'll work well for now (a reasoning-backed approach will come in a few weeks). There is a lot of experimentation we could do here, a better chunking will result in better ``references`` generation and so on. + +So this is our current process + - if normal function/class definition: no splitting, chunk as it is. + - if rst files, use the ``rst parser`` and split them with a chunk size of ~8000 tokens (max llama could take). RST files in FURY contain documentation & blog posts. + - if tutorial, try chunking as it is, if not possible split at 8000 tokens. + +Function/class definitions are generally under 8000 so I've not done explicit checks for now, the model will trim the remaining if longer (I found some long classes later). + +2) **Move colab files to a proper Python script** + +I did all the upsertion and experiments on colab. It is messy and can't be used in production. We need a one-click approach to upsertion. Something like point to `fury` directory and it should do everything. So I took the messy colab code and made a python script from it. + +One of my key goals is to separate core application logic from LLMs/Database providers. We should be able to swap them as needed without much fuss. I'll talk more about this in week 5. + +3) **Taking the bot public!** + +The whole point of making the bot is to help improve the productivity of FURY developers. So I decided to take it public on `this discord server `_. You could use it today! (actually, you could've used it from the 20th of last month, this blog got delayed😢) + +I'll observe what people are asking and then iterate towards making the bot better in that area. I think it'll be better than making the bot good on what I believe is the best. + +4) **Minor bugfixes and stuff** + +Did some minor bug fixes on things like the Discord bot generation cutoff and error handling improvements. It was Discord message limit (<=2000) that caused the generation to cut off, I split the message into parts to fix that. Error handling was improved generally everywhere. I'll need to bring logging later. + + +Minor Sidequest +~~~~~~~~~~~~~~~ + +This is in no way related to FURY, but it was fun so I thought I'd add it here :) + +So after midterms, I decided to go back home, to maximally use my time I searched for things to do and found a local FOSS event (`link `_). It was done by FOSS United Kochi and it's one of the major FOSS events in my state (Kerala, India). Met some Pythonistas! Explained what FURY is to them. I also ended up finding some lore (`link `_) about how GNU/Linux spread in Kerala, India. Also found some old FOSS event pictures (`this `_ one is talking about Python, 2003 World of Python). This was my first FOSS event outside campus so it was fun :) + + +What is coming up next week? +---------------------------- + +- Benchmarking +- Architecture Update + +Did you get stuck anywhere? +--------------------------- + +No, I did not get stuck. This week was more of learning and experimentation so I think it's normal what I encountered. + +LINKS: + +- `Discord Server `_ +- `A Text Splitting Guide `_ +- `GNU Case of Kerala `_ +- `2003 World of Python `_ +- `FOSS United Kochi `_ +- `Robin :) `_ + +Thank you for reading! diff --git a/docs/source/posts/2024/2024-07-01-week-5-robin.rst b/docs/source/posts/2024/2024-07-01-week-5-robin.rst new file mode 100644 index 000000000..b785be5f0 --- /dev/null +++ b/docs/source/posts/2024/2024-07-01-week-5-robin.rst @@ -0,0 +1,129 @@ +Week 5: LLM Benchmarking & Architecture Modifications +===================================================== + +.. post:: July 1 2024 + :author: Robin Roy + :tags: google + :category: gsoc + +Hi, I'm `Robin `_ and this is my blog about week 5. + +This week, we'll take all the things we did in the previous weeks, and quantify them. Benchmarking an LLM is the process of grading the LLM answer. To grade properly, we need good rubrics, so that's what I worked on this week. Also, I made some architectural changes, to make the overall development simple. + +Things I did in Week 5 +---------------------- + +1) **Architectural Update** + +Earlier, this was our architecture: + + + .. raw:: html + + + +This had an obvious issue, all the core logic was inside the Discord Bot. So if I want to say, use the LLM inference for making a GitHub bot, or for benchmarking etc, it wasn't possible. So I decided to cut the LLM logic from Discord Bot and made a new ``LLM Router``. It'll handle all the LLM logic from now on, and we do not directly call any other endpoint other than this one. +It makes life simple, every input going into the endpoint goes like this: + +.. code-block:: json + + { + "query": "Render a cube in fury", + "llm": "llama3-70b-8192", + "knn": "3", + "stream": False + } + +Every response coming out will be like this: + +.. code-block:: json + + { + "response": "Yes, this is how it would be done python import fury....", + "references": "1, 2, 3" + } + +What happens on the inside is completely abstracted away. You just call this and it'll + - call the embedding model + - pass embeddings to the database + - return them to LLM (which you can choose) + - returns LLM answer with references to you + +Currently, we support ``ollama``, ``google`` and ``groq`` providers. That itself is 20+ LLM support, and you could swap between them using ``/api/groq or api/google or /api/ollama ...``. Adding another provider is simply adding another endpoint. + +So if you do + +`curl -X POST https://robinroy03-fury-engine.hf.space/api/groq/generate -H "Content-Type: application/json" -d '{"query": "How do I create a sphere in FURY?", "llm": "llama3-70b-8192", "knn": "3", "stream": false}'` + + +You'll get a response from ``llama3-70b-8192`` using ``groq``. If you do ``https://robinroy03-fury-engine.hf.space/api/google/generate`` you can call any google gemini modes like ``gemini-1.5-pro`` or ``gemini-1.5-flash``. Same for ``ollama``. + +This still could be improved, it does not currently account for vision models. I did not add that because we do not use vision models other than for benchmarking now, and that too is done locally. Benchmarking could also be streamlined, I avoided that because benchmarking is still in development so I'll have to rewrite every day. Presently you can use this core ``router`` for a working LLM generation (you'll get the same thing you'll get from the Discord Bot. So if you have a website, all you have to do is call the API). + +This is our present architecture: + +.. image:: /_static/images/gsoc_llm_robin_week5.jpg + :alt: Present LLM architecture. + +It is the same thing as above, except we have two new components - ``LLM Engine`` and a ``Groq & Gemini`` endpoint. When we'll end up having a conversational model setup (right now, it is one question and one answer), this model will be upgraded to accommodate that. My plan is to extend LLM Engine and add that. Other features such as vision also could be added to this as needed. + +2) **Gemini Models added** + +As mentioned above, I added ``Gemini`` models this week. They have a decent free tier. Also, I'm studying the feasibility of fine-tuning using the ``Gemini`` models. + +3) **LLM Benchmarking** + +LLM Benchmarking is the process of evaluating the LLM output and giving a score. With this, making the model better will be simply a function of increasing the score. This area is still under development and the things I've tried here are the current standard procedures. To understand more about benchmarking, you can read `this `_, `this `_ and `this `_. This `course `_ is also amazing. + +I'll anyways give a TL;DR: +LLM benchmarking is essentially like writing an English Literature exam and getting the grades. Your evaluator may give you a 4 or a 5, and the reasoning can be varied. For the same answer, you may even get very varied results from 2 different evaluators! Two common rubrics they use are ``groundedness (whether the answer follows from the material)`` and ``completion (whether the answer is complete, whether it fully answers the question with respect to the material)``. These are the same rubrics we'll use for LLM evaluation. For code, it's different. The code should compile and do exactly what it should. + +Now FURY Bot does 2 things - writing code & writing answers for common questions (on GitHub issues etc). Presently, I've only collected data for coding questions, as they are much easier to evaluate and give a clear sense of direction (also I found more coding data). + +Evaluating FURY code can be done by: + 1) Running the code. + 2) Checking the output. + +Now we do this using ``pytest`` in the FURY repo for tests. But this approach is tedious, as collecting questions and writing test cases take a lot of time, also the orientation of the 3D objects also matters (an LLM generation is not deterministic). So we are using a vision model ``moondream2`` to check the LLM generated output and verify if it is what we actually wanted. +On a high level, this is what we do (for now): + +- Take a QnA pair from the collected dataset (I've collected ~23 questions). +- Ask the LLM to generate a FURY code for that (using the references). +- Run this generated code. +- Check the output using ``moondream2`` and verify whether it is what we wanted. + +There is also ``fast_eval`` which checks whether the code compiles and skips ``moondream2`` entirely. This is obviously faster and is also decently good (is actually a pretty good heuristic). If it runs, assume it works :) + +This is our current stats: (from now on, we can finally talk using numbers) + +Coding benchmark: +~~~~~~~~~~~~~~~~~ +On ``fast_eval`` we have a success rate of ``47.83%`` for ``groq``. + +On ``normal_eval`` we have a success rate of ``13.04%`` for ``groq``. + +Note that ``moondream2`` also sometimes mistakes the output for something else. It is close to ``~45%`` when I checked manually. For now, I'm only going to focus on ``fast_eval`` as fixing ``moondream2`` is a distraction for the moment. (This actually gets very meta, there are projects where they have benchmarks for the evaluator and so on. `Read this `_.) + + +What is coming up next week? +---------------------------- + +- Better benchmark scores :) +- Line number highlighting @ references. +- Some ``references`` improvements. + +Did you get stuck anywhere? +--------------------------- + +No, I did not get stuck anywhere. + +LINKS: + +- `RAG Evaluation `_ +- `LLM Judge `_ +- `Advanced RAG `_ +- `Advanced Retrieval for AI `_ +- `Moondream2 `_ +- `Finding GPT-4 mistakes with GPT-4 `_ + +Thank you for reading! From 7651a24fc388082572042a486ea8e718a22f4539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wachiou=20BOURA=C3=8FMA?= <100234404+WassCodeur@users.noreply.github.com> Date: Wed, 10 Jul 2024 07:49:49 +0000 Subject: [PATCH 10/11] DOC: add Wachiou's week5 Blog post (#908) * DOC: add Wachiou's week5 Blog post * DOC: Clarification de la ligne 36 --- .../2024-07-06-week5-wachiou-bouraima.rst | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/source/posts/2024/2024-07-06-week5-wachiou-bouraima.rst diff --git a/docs/source/posts/2024/2024-07-06-week5-wachiou-bouraima.rst b/docs/source/posts/2024/2024-07-06-week5-wachiou-bouraima.rst new file mode 100644 index 000000000..01a915125 --- /dev/null +++ b/docs/source/posts/2024/2024-07-06-week5-wachiou-bouraima.rst @@ -0,0 +1,112 @@ +WEEK 5: Implementing Lazy Loading in FURY with ``lazy_loader`` +============================================================== + +.. post:: July 6, 2024 + :author: Wachiou BOURAIMA + :tags: google + :category: gsoc + +Hello everyone, +--------------- + +Welcome back to my Google Summer of Code (GSoC) 2024 journey! This week has been particularly exciting as I introduced a significant performance optimization feature: lazy loading. Here's an overview of my progress and contributions. + + +**Introduction of lazy loading** +-------------------------------- + +This week, I focused on implementing the ``lazy_loader`` feature of `Scientific Python `_ to optimize module loading in FURY. Lazy loading improves performance by deferring the loading of modules until they are actually needed, thus reducing start-up times and memory footprint. + +The implementation involved: + +1. Implementation of Lazy Loading: + + - Application of lazy loading in several FURY modules using the ``lazy_loader`` module to improve performance + +2. Update ``__init__.py`` files: + + - Modified ``__init__.py`` files to support lazy loading where necessary. This ensures that modules are only loaded when they are accessed for the first time + +3. Added Type Stubs (``__init__.pyi``): + + - Adding type stubs (``__init__.pyi``) provides type hints for lazy-loading modules, improving code readability and maintainability + +4. **Improved module organization:** + + - Improved module organization in ``__init__.py`` and ``__init__.pyi`` files, to effectively support lazy loading. + + +**Example Implementation** +--------------------------- + +To give you an idea, here's the actual implementation of how lazy loading was done using the ``lazy_loader`` module in FURY: + +``__init__.py`` File: + +.. code-block:: python + + import lazy_loader as lazy + from fury.pkg_info import __version__, pkg_commit_hash + + __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) + + _all__ += [ + "__version__", + "disable_warnings", + "enable_warnings", + "get_info", + ] + + # ... (functions) + +``__init__.pyi`` File: + +.. code-block:: python + + # This file is a stub type for the fury package. It provides information about types + # to help type-checking tools like mypy and improve the development experience + # with better autocompletion and documentation in code editors. + + __all__ = [ + "actor", + "actors", + "animation", + "colormap", + # ... (other modules) + , + ] + + from . import ( + actor, + actors, + animation, + colormap, + # ... (other modules) + , + ) + # ... (other functions) + +You can review the implementation in `this pull request `_. + + +Reading ``SPEC1`` +----------------- + +To align myself with best practice, I read the `SPEC1 `_ document available at Scientific Python SPEC1. This document provided valuable hints and guidelines that I took into account when implementing the lazy loading feature. + + +Did I get stuck anywhere? +-------------------------- +No, I didn't encounter any major blockers this week. The implementation of lazy loading went smoothly, and I was able to complete the task. + + +**What's Next?** +----------------- + +For the next week, I plan to: + +1. Review all my Pull Requests with my mentor `Serge Koudoro `_, to ensure everything is up to FURY's standards. +2. Start working on the redesign of the FURY website, making it more user-friendly and visually appealing. + + +Thank you for reading. Stay tuned for more updates on my progress! From 56a6177615c32dcaaf758d2360877216c8356180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wachiou=20BOURA=C3=8FMA?= <100234404+WassCodeur@users.noreply.github.com> Date: Wed, 10 Jul 2024 08:33:37 +0000 Subject: [PATCH 11/11] NF: Add keyword_only decorator to enforce keyword-only arguments (#888) * parent 4baa99d82b57561f41c39cdab9c6d0bbac28415a author WassCodeur 1716302141 +0000 committer WassCodeur 1717549770 +0000 NF: Add keyword_only decorator to enforce keyword-only arguments RF: Refactor keyword-only code and Add keyword-only arguments to functions TEST: Add test for keyword_only" decorator Refactor code to use keyword-only arguments in several functions NF: remove some doctest and add a short comment on line 111 to explain what the loop does in fury/decorators.py RF: Refactor keyword-only code and Add keyword-only arguments to functions Refactor code to use keyword-only arguments in several functions * NF: Add keyword_only decorator to enforce keyword-only arguments * NF: Add keyword_only decorator to enforce keyword-only arguments * RF: Refactor keyword-only code and Add keyword-only arguments to functions * TEST: Add test for keyword_only" decorator * Refactor interactor.py and molecular.py * RF: Rename and update decorator for version checks - Renamed `keyword-only` decorator to `warn_on_args_to_kwargs` for greater clarity. - Updated `warn_on_args_to_kwargs` to include version parameters `from_version` and `until_version`. - Added logic to raise a RuntimeError if the current version of FURY_VERSION is greater than `until_version`. - Moved `__version__` definition to a new `fury/version.py` module to avoid circular import problems. - Updated all functions using the decorator to reflect the new name and parameters. - Adjusted import declarations and ensured compatibility across the code base. * RF: Refactor the code by removing version constraints in the call to the warn_on_args_to_kwargs decorator * RF: Refactor warn_on_args_to_kwargs code and Add warn_on_args_to_kwargs arguments to functions in window.py and actor.py * RF: update the version checks in the decorator: warn_on_args_to_kwargs and update the unittest * RF: warn_on_args_to_kwargs decorator --- fury/actor.py | 259 +++++++++++++++++------- fury/actors/odf_slicer.py | 4 +- fury/colormap.py | 28 ++- fury/convert.py | 10 +- fury/decorators.py | 161 +++++++++++++++ fury/gltf.py | 79 +++++--- fury/interactor.py | 12 +- fury/io.py | 45 +++- fury/layout.py | 78 +++++-- fury/material.py | 59 +++++- fury/molecular.py | 80 ++++++-- fury/optpkg.py | 7 +- fury/pick.py | 50 ++++- fury/pkg_info.py | 3 + fury/primitive.py | 42 ++-- fury/shaders/tests/test_base.py | 2 +- fury/testing.py | 23 ++- fury/tests/test_actors.py | 177 ++++++++++------ fury/tests/test_colormap.py | 26 ++- fury/tests/test_decorators.py | 46 ++++- fury/tests/test_deprecator.py | 14 +- fury/tests/test_gltf.py | 14 +- fury/tests/test_interactor.py | 13 +- fury/tests/test_io.py | 79 +++++++- fury/tests/test_layout.py | 28 ++- fury/tests/test_molecular.py | 120 +++++++---- fury/tests/test_primitive.py | 25 ++- fury/tests/test_stream.py | 20 +- fury/tests/test_testing.py | 33 ++- fury/tests/test_thread.py | 2 +- fury/tests/test_transform.py | 8 +- fury/tests/test_utils.py | 47 +++-- fury/tests/test_window.py | 34 ++-- fury/transform.py | 10 +- fury/ui/core.py | 4 +- fury/ui/helpers.py | 17 +- fury/ui/tests/test_containers.py | 50 ++++- fury/ui/tests/test_elements.py | 2 +- fury/ui/tests/test_elements_callback.py | 11 +- fury/ui/tests/test_helpers.py | 14 +- fury/utils.py | 57 ++++-- fury/window.py | 63 ++++-- 42 files changed, 1393 insertions(+), 463 deletions(-) diff --git a/fury/actor.py b/fury/actor.py index 3094ef112..346373bc9 100644 --- a/fury/actor.py +++ b/fury/actor.py @@ -15,6 +15,7 @@ tensor_ellipsoid, ) from fury.colormap import colormap_lookup_table +from fury.decorators import warn_on_args_to_kwargs from fury.deprecator import deprecate_with_version, deprecated_params from fury.io import load_image from fury.lib import ( @@ -88,8 +89,10 @@ ) +@warn_on_args_to_kwargs() def slicer( data, + *, affine=None, value_range=None, opacity=1.0, @@ -265,7 +268,8 @@ def display_extent(self, x1, x2, y1, y2, z1, z2): # line = np.array([[xmin, ymin, zmin]]) # self.outline_actor = actor.line() - def display(self, x=None, y=None, z=None): + @warn_on_args_to_kwargs() + def display(self, *, x=None, y=None, z=None): if x is None and y is None and z is None: self.display_extent(ex1, ex2, ey1, ey2, ez2 // 2, ez2 // 2) if x is not None: @@ -321,7 +325,12 @@ def shallow_copy(self): lut = lookup_colormap if lookup_colormap is None: # Create a black/white lookup table. - lut = colormap_lookup_table((r1, r2), (0, 0), (0, 0), (0, 1)) + lut = colormap_lookup_table( + scale_range=(r1, r2), + hue_range=(0, 0), + saturation_range=(0, 0), + value_range=(0, 1), + ) plane_colors = ImageMapToColors() plane_colors.SetOutputFormatToRGB() @@ -345,7 +354,8 @@ def shallow_copy(self): return image_actor -def surface(vertices, faces=None, colors=None, smooth=None, subdivision=3): +@warn_on_args_to_kwargs() +def surface(vertices, *, faces=None, colors=None, smooth=None, subdivision=3): """Generate a surface actor from an array of vertices. The color and smoothness of the surface can be customized by specifying @@ -422,7 +432,8 @@ def surface(vertices, faces=None, colors=None, smooth=None, subdivision=3): return surface_actor -def contour_from_roi(data, affine=None, color=None, opacity=1): +@warn_on_args_to_kwargs() +def contour_from_roi(data, *, affine=None, color=None, opacity=1): """Generate surface actor from a binary ROI. The color and opacity of the surface can be customized. @@ -541,7 +552,8 @@ def contour_from_roi(data, affine=None, color=None, opacity=1): return skin_actor -def contour_from_label(data, affine=None, color=None): +@warn_on_args_to_kwargs() +def contour_from_label(data, *, affine=None, color=None): """Generate surface actor from a labeled Array. The color and opacity of individual surfaces can be customized. @@ -586,15 +598,17 @@ def contour_from_label(data, affine=None, color=None): for i, roi_id in enumerate(unique_roi_id): roi_data = np.isin(data, roi_id).astype(int) roi_surface = contour_from_roi( - roi_data, affine, color=color[i], opacity=opacity[i] + roi_data, affine=affine, color=color[i], opacity=opacity[i] ) unique_roi_surfaces.AddPart(roi_surface) return unique_roi_surfaces +@warn_on_args_to_kwargs() def streamtube( lines, + *, colors=None, opacity=1, linewidth=0.1, @@ -662,7 +676,7 @@ def streamtube( >>> scene = window.Scene() >>> lines = [np.random.rand(10, 3), np.random.rand(20, 3)] >>> colors = np.random.rand(2, 3) - >>> c = actor.streamtube(lines, colors) + >>> c = actor.streamtube(lines, colores=colors) >>> scene.add(c) >>> #window.show(scene) @@ -687,7 +701,7 @@ def streamtube( """ # Poly data with lines and colors - poly_data, color_is_scalar = lines_to_vtk_polydata(lines, colors) + poly_data, color_is_scalar = lines_to_vtk_polydata(lines, colors=colors) next_input = poly_data # set primitives count @@ -760,8 +774,10 @@ def streamtube( return actor +@warn_on_args_to_kwargs() def line( lines, + *, colors=None, opacity=1, linewidth=1, @@ -832,13 +848,13 @@ def line( >>> scene = window.Scene() >>> lines = [np.random.rand(10, 3), np.random.rand(20, 3)] >>> colors = np.random.rand(2, 3) - >>> c = actor.line(lines, colors) + >>> c = actor.line(lines, colors=colors) >>> scene.add(c) >>> #window.show(scene) """ # Poly data with lines and colors - poly_data, color_is_scalar = lines_to_vtk_polydata(lines, colors) + poly_data, color_is_scalar = lines_to_vtk_polydata(lines, colors=colors) next_input = poly_data # set primitives count @@ -882,7 +898,8 @@ def line( if depth_cue: - def callback(_caller, _event, calldata=None): + @warn_on_args_to_kwargs() + def callback(_caller, _event, *, calldata=None): program = calldata if program is not None: program.SetUniformf("linewidth", linewidth) @@ -896,7 +913,8 @@ def callback(_caller, _event, calldata=None): return actor -def scalar_bar(lookup_table=None, title=" "): +@warn_on_args_to_kwargs() +def scalar_bar(*, lookup_table=None, title=" "): """Default scalar bar actor for a given colormap (colorbar). Parameters @@ -927,8 +945,14 @@ def scalar_bar(lookup_table=None, title=" "): return scalar_bar +@warn_on_args_to_kwargs() def axes( - scale=(1, 1, 1), colorx=(1, 0, 0), colory=(0, 1, 0), colorz=(0, 0, 1), opacity=1 + *, + scale=(1, 1, 1), + colorx=(1, 0, 0), + colory=(0, 1, 0), + colorz=(0, 0, 1), + opacity=1, ): """Create an actor with the coordinate's system axes where red = x, green = y, blue = z. @@ -956,12 +980,14 @@ def axes( colors = np.array([colorx + (opacity,), colory + (opacity,), colorz + (opacity,)]) scales = np.asarray(scale) - arrow_actor = arrow(centers, dirs, colors, scales, repeat_primitive=False) + arrow_actor = arrow(centers, dirs, colors, scales=scales, repeat_primitive=False) return arrow_actor +@warn_on_args_to_kwargs() def odf_slicer( odfs, + *, affine=None, mask=None, sphere=None, @@ -1032,7 +1058,7 @@ def odf_slicer( if sphere is None: # Use a default sphere with 100 vertices - vertices, faces = fp.prim_sphere("repulsion100") + vertices, faces = fp.prim_sphere(name="repulsion100") else: vertices = sphere.vertices faces = fix_winding_order(vertices, sphere.faces, clockwise=True) @@ -1078,7 +1104,8 @@ def _makeNd(array, ndim): return array.reshape(new_shape) -def _roll_evals(evals, axis=-1): +@warn_on_args_to_kwargs() +def _roll_evals(evals, *, axis=-1): """Check evals shape. Helper function to check that the evals provided to functions calculating @@ -1107,7 +1134,8 @@ def _roll_evals(evals, axis=-1): return evals -def _fa(evals, axis=-1): +@warn_on_args_to_kwargs() +def _fa(evals, *, axis=-1): r"""Return Fractional anisotropy (FA) of a diffusion tensor. Parameters @@ -1133,7 +1161,7 @@ def _fa(evals, axis=-1): \lambda_2^2+\lambda_3^2}} """ - evals = _roll_evals(evals, axis) + evals = _roll_evals(evals, axis=axis) # Make sure not to get nans all_zero = (evals == 0).all(axis=0) ev1, ev2, ev3 = evals @@ -1178,9 +1206,11 @@ def _color_fa(fa, evecs): return np.abs(evecs[..., 0]) * np.clip(fa, 0, 1)[..., None] +@warn_on_args_to_kwargs() def tensor_slicer( evals, evecs, + *, affine=None, mask=None, sphere=None, @@ -1256,7 +1286,8 @@ def display_extent(self, x1, x2, y1, y2, z1, z2): ) self.SetMapper(self.mapper) - def display(self, x=None, y=None, z=None): + @warn_on_args_to_kwargs() + def display(self, *, x=None, y=None, z=None): if x is None and y is None and z is None: self.display_extent( 0, @@ -1283,9 +1314,11 @@ def display(self, x=None, y=None, z=None): return tensor_actor +@warn_on_args_to_kwargs() def _tensor_slicer_mapper( evals, evecs, + *, affine=None, mask=None, sphere=None, @@ -1389,8 +1422,10 @@ def _tensor_slicer_mapper( return mapper +@warn_on_args_to_kwargs() def peak_slicer( peaks_dirs, + *, peaks_values=None, mask=None, affine=None, @@ -1510,7 +1545,8 @@ def display_extent(self, x1, x2, y1, y2, z1, z2): self.SetProperty(self.line.GetProperty()) self.SetMapper(self.line.GetMapper()) - def display(self, x=None, y=None, z=None): + @warn_on_args_to_kwargs() + def display(self, *, x=None, y=None, z=None): if x is None and y is None and z is None: self.display_extent( 0, @@ -1537,8 +1573,10 @@ def display(self, x=None, y=None, z=None): return peak_actor +@warn_on_args_to_kwargs() def peak( peaks_dirs, + *, peaks_values=None, mask=None, affine=None, @@ -1652,7 +1690,8 @@ def peak( ) -def dot(points, colors=None, opacity=None, dot_size=5): +@warn_on_args_to_kwargs() +def dot(points, *, colors=None, opacity=None, dot_size=5): """Create one or more 3d points. Parameters @@ -1699,7 +1738,7 @@ def dot(points, colors=None, opacity=None, dot_size=5): vtk_faces.InsertNextCell(1) vtk_faces.InsertCellPoint(idd) - color_tuple = color_check(len(points), colors) + color_tuple = color_check(len(points), colors=colors) color_array, global_opacity = color_tuple # Create a polydata object @@ -1734,7 +1773,8 @@ def dot(points, colors=None, opacity=None, dot_size=5): )(dot) -def point(points, colors, point_radius=0.1, phi=8, theta=8, opacity=1.0): +@warn_on_args_to_kwargs() +def point(points, colors, *, point_radius=0.1, phi=8, theta=8, opacity=1.0): """Visualize points as sphere glyphs. Parameters @@ -1778,9 +1818,11 @@ def point(points, colors, point_radius=0.1, phi=8, theta=8, opacity=1.0): ) +@warn_on_args_to_kwargs() def sphere( centers, colors, + *, radii=1.0, phi=16, theta=16, @@ -1863,16 +1905,18 @@ def sphere( big_verts, big_faces, big_colors, _ = res prim_count = len(centers) sphere_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) sphere_actor.GetProperty().SetOpacity(opacity) return sphere_actor +@warn_on_args_to_kwargs() def cylinder( centers, directions, colors, + *, radius=0.05, heights=1, capped=False, @@ -1948,7 +1992,7 @@ def cylinder( big_verts, big_faces, big_colors, _ = res prim_count = len(centers) cylinder_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) else: @@ -1976,10 +2020,12 @@ def cylinder( return cylinder_actor +@warn_on_args_to_kwargs() def disk( centers, directions, colors, + *, rinner=0.3, router=0.7, cresolution=6, @@ -2053,7 +2099,8 @@ def disk( return disk_actor -def square(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): +@warn_on_args_to_kwargs() +def square(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): """Visualize one or many squares with different features. Parameters @@ -2077,7 +2124,7 @@ def square(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): >>> scene = window.Scene() >>> centers = np.random.rand(5, 3) >>> dirs = np.random.rand(5, 3) - >>> sq_actor = actor.square(centers, dirs) + >>> sq_actor = actor.square(centers, directions=dirs) >>> scene.add(sq_actor) >>> # window.show(scene) @@ -2095,13 +2142,14 @@ def square(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) sq_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) sq_actor.GetProperty().BackfaceCullingOff() return sq_actor -def rectangle(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 0)): +@warn_on_args_to_kwargs() +def rectangle(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 0)): """Visualize one or many rectangles with different features. Parameters @@ -2129,7 +2177,7 @@ def rectangle(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 0)) >>> scene = window.Scene() >>> centers = np.random.rand(5, 3) >>> dirs = np.random.rand(5, 3) - >>> rect_actor = actor.rectangle(centers, dirs) + >>> rect_actor = actor.rectangle(centers, directions=dirs) >>> scene.add(rect_actor) >>> # window.show(scene) @@ -2137,8 +2185,9 @@ def rectangle(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 0)) return square(centers=centers, directions=directions, colors=colors, scales=scales) +@warn_on_args_to_kwargs() @deprecated_params(["size", "heights"], ["scales", "scales"], since="0.6", until="0.8") -def box(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 3)): +def box(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 3)): """Visualize one or many boxes with different features. Parameters @@ -2162,7 +2211,7 @@ def box(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 3)): >>> scene = window.Scene() >>> centers = np.random.rand(5, 3) >>> dirs = np.random.rand(5, 3) - >>> box_actor = actor.box(centers, dirs, (1, 1, 1)) + >>> box_actor = actor.box(centers, directions=dirs, colors=(1, 1, 1)) >>> scene.add(box_actor) >>> # window.show(scene) @@ -2180,13 +2229,14 @@ def box(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=(1, 2, 3)): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) box_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return box_actor +@warn_on_args_to_kwargs() @deprecated_params("heights", "scales", since="0.6", until="0.8") -def cube(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): +def cube(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): """Visualize one or many cubes with different features. Parameters @@ -2210,7 +2260,7 @@ def cube(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): >>> scene = window.Scene() >>> centers = np.random.rand(5, 3) >>> dirs = np.random.rand(5, 3) - >>> cube_actor = actor.cube(centers, dirs) + >>> cube_actor = actor.cube(centers, directions=dirs) >>> scene.add(cube_actor) >>> # window.show(scene) @@ -2218,10 +2268,12 @@ def cube(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): return box(centers=centers, directions=directions, colors=colors, scales=scales) +@warn_on_args_to_kwargs() def arrow( centers, directions, colors, + *, heights=1.0, resolution=10, tip_length=0.35, @@ -2270,7 +2322,7 @@ def arrow( >>> centers = np.random.rand(5, 3) >>> directions = np.random.rand(5, 3) >>> heights = np.random.rand(5) - >>> arrow_actor = actor.arrow(centers, directions, (1, 1, 1), heights) + >>> arrow_actor = actor.arrow(centers, directions, (1, 1, 1), heights=heights) >>> scene.add(arrow_actor) >>> # window.show(scene) @@ -2288,7 +2340,7 @@ def arrow( big_vertices, big_faces, big_colors, _ = res prim_count = len(centers) arrow_actor = get_actor_from_primitive( - big_vertices, big_faces, big_colors, prim_count=prim_count + big_vertices, big_faces, colors=big_colors, prim_count=prim_count ) return arrow_actor @@ -2313,10 +2365,12 @@ def arrow( return arrow_actor +@warn_on_args_to_kwargs() def cone( centers, directions, colors, + *, heights=1.0, resolution=10, vertices=None, @@ -2357,7 +2411,7 @@ def cone( >>> centers = np.random.rand(5, 3) >>> directions = np.random.rand(5, 3) >>> heights = np.random.rand(5) - >>> cone_actor = actor.cone(centers, directions, (1, 1, 1), heights) + >>> cone_actor = actor.cone(centers, directions, (1, 1, 1), heights=heights) >>> scene.add(cone_actor) >>> # window.show(scene) @@ -2389,13 +2443,14 @@ def cone( big_verts, big_faces, big_colors, _ = res prim_count = len(centers) cone_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return cone_actor -def triangularprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): +@warn_on_args_to_kwargs() +def triangularprism(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): """Visualize one or many regular triangular prisms with different features. Parameters @@ -2421,11 +2476,11 @@ def triangularprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): >>> dirs = np.random.rand(3, 3) >>> colors = np.random.rand(3, 3) >>> scales = np.random.rand(3, 1) - >>> actor = actor.triangularprism(centers, dirs, colors, scales) + >>> actor = actor.triangularprism(centers, directions=dirs, colors=colors, scales=scales) >>> scene.add(actor) >>> # window.show(scene) - """ + """ # noqa: E501 verts, faces = fp.prim_triangularprism() res = fp.repeat_primitive( verts, @@ -2438,12 +2493,13 @@ def triangularprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) tprism_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return tprism_actor -def rhombicuboctahedron(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): +@warn_on_args_to_kwargs() +def rhombicuboctahedron(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): """Visualize one or many rhombicuboctahedron with different features. Parameters @@ -2469,11 +2525,11 @@ def rhombicuboctahedron(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales= >>> dirs = np.random.rand(3, 3) >>> colors = np.random.rand(3, 3) >>> scales = np.random.rand(3, 1) - >>> actor = actor.rhombicuboctahedron(centers, dirs, colors, scales) + >>> actor = actor.rhombicuboctahedron(centers, directions=dirs, colors=colors, scales=scales) >>> scene.add(actor) >>> # window.show(scene) - """ + """ # noqa: E501 verts, faces = fp.prim_rhombicuboctahedron() res = fp.repeat_primitive( verts, @@ -2486,12 +2542,13 @@ def rhombicuboctahedron(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales= big_verts, big_faces, big_colors, _ = res prim_count = len(centers) rcoh_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return rcoh_actor -def pentagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): +@warn_on_args_to_kwargs() +def pentagonalprism(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): """Visualize one or many pentagonal prisms with different features. Parameters @@ -2518,11 +2575,11 @@ def pentagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): >>> dirs = np.random.rand(3, 3) >>> colors = np.random.rand(3, 3) >>> scales = np.random.rand(3, 1) - >>> actor_pentagonal = actor.pentagonalprism(centers, dirs, colors, scales) + >>> actor_pentagonal = actor.pentagonalprism(centers, directions=dirs, colors=colors, scales=scales) >>> scene.add(actor_pentagonal) >>> # window.show(scene) - """ + """ # noqa: E501 verts, faces = fp.prim_pentagonalprism() res = fp.repeat_primitive( verts, @@ -2536,12 +2593,13 @@ def pentagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) pent_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return pent_actor -def octagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): +@warn_on_args_to_kwargs() +def octagonalprism(centers, *, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): """Visualize one or many octagonal prisms with different features. Parameters @@ -2567,11 +2625,11 @@ def octagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): >>> dirs = np.random.rand(3, 3) >>> colors = np.random.rand(3, 3) >>> scales = np.random.rand(3, 1) - >>> actor = actor.octagonalprism(centers, dirs, colors, scales) + >>> actor = actor.octagonalprism(centers, directions=dirs, colors=colors, scales=scales) >>> scene.add(actor) >>> # window.show(scene) - """ + """ # noqa: E501 verts, faces = fp.prim_octagonalprism() res = fp.repeat_primitive( verts, @@ -2585,12 +2643,13 @@ def octagonalprism(centers, directions=(1, 0, 0), colors=(1, 0, 0), scales=1): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) oct_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return oct_actor -def frustum(centers, directions=(1, 0, 0), colors=(0, 1, 0), scales=1): +@warn_on_args_to_kwargs() +def frustum(centers, *, directions=(1, 0, 0), colors=(0, 1, 0), scales=1): """Visualize one or many frustum pyramids with different features. Parameters @@ -2616,7 +2675,7 @@ def frustum(centers, directions=(1, 0, 0), colors=(0, 1, 0), scales=1): >>> dirs = np.random.rand(4, 3) >>> colors = np.random.rand(4, 3) >>> scales = np.random.rand(4, 1) - >>> actor = actor.frustum(centers, dirs, colors, scales) + >>> actor = actor.frustum(centers, directions=dirs, colors=colors, scales=scales) >>> scene.add(actor) >>> # window.show(scene) @@ -2634,13 +2693,14 @@ def frustum(centers, directions=(1, 0, 0), colors=(0, 1, 0), scales=1): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) frustum_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return frustum_actor +@warn_on_args_to_kwargs() def superquadric( - centers, roundness=(1, 1), directions=(1, 0, 0), colors=(1, 0, 0), scales=1 + centers, *, roundness=(1, 1), directions=(1, 0, 0), colors=(1, 0, 0), scales=1 ): """Visualize one or many superquadrics with different features. @@ -2705,13 +2765,15 @@ def have_2_dimensions(arr): big_verts, big_faces, big_colors, _ = res prim_count = len(centers) spq_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) return spq_actor +@warn_on_args_to_kwargs() def billboard( centers, + *, colors=(0, 1, 0), scales=1, vs_dec=None, @@ -2766,7 +2828,7 @@ def billboard( prim_count = len(centers) bb_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) bb_actor.GetMapper().SetVBOShiftScaleMethod(False) bb_actor.GetProperty().BackfaceCullingOff() @@ -2859,7 +2921,9 @@ def billboard( return bb_actor +@warn_on_args_to_kwargs() def vector_text( + *, text="Origin", pos=(0, 0, 0), scale=(0.2, 0.2, 0.2), @@ -2965,8 +3029,10 @@ def add_to_scene(scene): )(vector_text) +@warn_on_args_to_kwargs() def text_3d( text, + *, position=(0, 0, 0), color=(1, 1, 1), font_size=12, @@ -3014,7 +3080,8 @@ def font_size(self, size): self.GetTextProperty().SetFontSize(24) text_actor.SetScale((1.0 / 24.0 * size,) * 3) - def font_family(self, _family="Arial"): + @warn_on_args_to_kwargs() + def font_family(self, *, _family="Arial"): self.GetTextProperty().SetFontFamilyToArial() def justification(self, justification): @@ -3041,7 +3108,8 @@ def vertical_justification(self, justification): "Unknown vertical justification: '{}'".format(justification) ) - def font_style(self, bold=False, italic=False, shadow=False): + @warn_on_args_to_kwargs() + def font_style(self, *, bold=False, italic=False, shadow=False): tprop = self.GetTextProperty() if bold: tprop.BoldOn() @@ -3069,8 +3137,8 @@ def get_position(self): text_actor.message(text) text_actor.font_size(font_size) text_actor.set_position(position) - text_actor.font_family(font_family) - text_actor.font_style(bold, italic, shadow) + text_actor.font_family(_family=font_family) + text_actor.font_style(bold=bold, italic=italic, shadow=shadow) text_actor.color(color) text_actor.justification(justification) text_actor.vertical_justification(vertical_justification) @@ -3096,7 +3164,8 @@ class Container: """ - def __init__(self, layout=None): + @warn_on_args_to_kwargs() + def __init__(self, *, layout=None): """Parameters ---------- layout : ``fury.layout.Layout`` object @@ -3229,8 +3298,10 @@ def __len__(self): return len(self._items) +@warn_on_args_to_kwargs() def grid( actors, + *, captions=None, caption_offset=(0, -100, 0), cell_padding=0, @@ -3311,7 +3382,8 @@ def grid( return grid -def figure(pic, interpolation="nearest"): +@warn_on_args_to_kwargs() +def figure(pic, *, interpolation="nearest"): """Return a figure as an image actor. Parameters @@ -3357,7 +3429,8 @@ def figure(pic, interpolation="nearest"): return image_actor -def texture(rgb, interp=True): +@warn_on_args_to_kwargs() +def texture(rgb, *, interp=True): """Map an RGB or RGBA texture on a plane. Parameters @@ -3436,7 +3509,8 @@ def texture_update(texture_actor, arr): grid.GetPointData().SetScalars(vtkarr) -def _textured_sphere_source(theta=60, phi=60): +@warn_on_args_to_kwargs() +def _textured_sphere_source(*, theta=60, phi=60): """Use vtkTexturedSphereSource to set the theta and phi. Parameters @@ -3458,7 +3532,8 @@ def _textured_sphere_source(theta=60, phi=60): return tss -def texture_on_sphere(rgb, theta=60, phi=60, interpolate=True): +@warn_on_args_to_kwargs() +def texture_on_sphere(rgb, *, theta=60, phi=60, interpolate=True): """Map an RGB or RGBA texture on a sphere. Parameters @@ -3494,7 +3569,8 @@ def texture_on_sphere(rgb, theta=60, phi=60, interpolate=True): return earthActor -def texture_2d(rgb, interp=False): +@warn_on_args_to_kwargs() +def texture_2d(rgb, *, interp=False): """Create 2D texture from array. Parameters @@ -3560,7 +3636,10 @@ def texture_2d(rgb, interp=False): return act -def sdf(centers, directions=(1, 0, 0), colors=(1, 0, 0), primitives="torus", scales=1): +@warn_on_args_to_kwargs() +def sdf( + centers, *, directions=(1, 0, 0), colors=(1, 0, 0), primitives="torus", scales=1 +): """Create a SDF primitive based actor. Parameters @@ -3597,7 +3676,7 @@ def sdf(centers, directions=(1, 0, 0), colors=(1, 0, 0), primitives="torus", sca rep_verts, rep_faces, rep_colors, rep_centers = repeated prim_count = len(centers) box_actor = get_actor_from_primitive( - rep_verts, rep_faces, rep_colors, prim_count=prim_count + rep_verts, rep_faces, colors=rep_colors, prim_count=prim_count ) box_actor.GetMapper().SetVBOShiftScaleMethod(False) @@ -3641,8 +3720,10 @@ def sdf(centers, directions=(1, 0, 0), colors=(1, 0, 0), primitives="torus", sca return box_actor +@warn_on_args_to_kwargs() def markers( centers, + *, colors=(0, 1, 0), scales=1, marker="3d", @@ -3707,7 +3788,7 @@ def markers( big_verts, big_faces, big_colors, big_centers = res prim_count = len(centers) sq_actor = get_actor_from_primitive( - big_verts, big_faces, big_colors, prim_count=prim_count + big_verts, big_faces, colors=big_colors, prim_count=prim_count ) sq_actor.GetMapper().SetVBOShiftScaleMethod(False) sq_actor.GetProperty().BackfaceCullingOff() @@ -3780,7 +3861,12 @@ def markers( attribute_to_actor(sq_actor, list_of_markers, "marker") def callback( - _caller, _event, calldata=None, uniform_type="f", uniform_name=None, value=None + _caller, + _event, + calldata=None, + uniform_type="f", + uniform_name=None, + value=None, ): program = calldata if program is not None: @@ -3819,7 +3905,16 @@ def callback( return sq_actor -def ellipsoid(centers, axes, lengths, colors=(1, 0, 0), scales=1.0, opacity=1.0): +@warn_on_args_to_kwargs() +def ellipsoid( + centers, + axes, + lengths, + *, + colors=(1, 0, 0), + scales=1.0, + opacity=1.0, +): """VTK actor for visualizing ellipsoids. Parameters @@ -3884,7 +3979,17 @@ def ellipsoid(centers, axes, lengths, colors=(1, 0, 0), scales=1.0, opacity=1.0) return tensor_ellipsoid(centers, axes, lengths, colors, scales, opacity) -def uncertainty_cone(evals, evecs, signal, sigma, b_matrix, scales=0.6, opacity=1.0): +@warn_on_args_to_kwargs() +def uncertainty_cone( + evals, + evecs, + signal, + sigma, + b_matrix, + *, + scales=0.6, + opacity=1.0, +): """VTK actor for visualizing the cone of uncertainty representing the variance of the main direction of diffusion. diff --git a/fury/actors/odf_slicer.py b/fury/actors/odf_slicer.py index 8b75d200a..fefbd5abe 100644 --- a/fury/actors/odf_slicer.py +++ b/fury/actors/odf_slicer.py @@ -254,14 +254,14 @@ def _generate_color_for_vertices(self, sf): if self.colormap is None: raise IOError("if global_cm=True, colormap must be defined.") else: - all_colors = create_colormap(sf.ravel(), self.colormap) * 255 + all_colors = create_colormap(sf.ravel(), name=self.colormap) * 255 elif self.colormap is not None: if isinstance(self.colormap, str): # Map ODFs values [min, max] to [0, 1] for each ODF range_sf = sf.max(axis=-1) - sf.min(axis=-1) rescaled = sf - sf.min(axis=-1, keepdims=True) rescaled[range_sf > 0] /= range_sf[range_sf > 0][..., None] - all_colors = create_colormap(rescaled.ravel(), self.colormap) * 255 + all_colors = create_colormap(rescaled.ravel(), name=self.colormap) * 255 else: all_colors = np.tile( np.array(self.colormap).reshape(1, 3), diff --git a/fury/colormap.py b/fury/colormap.py index 4d9a5c0b1..10659bc99 100644 --- a/fury/colormap.py +++ b/fury/colormap.py @@ -6,6 +6,7 @@ from scipy import linalg from fury.data import DATA_DIR +from fury.decorators import warn_on_args_to_kwargs from fury.lib import LookupTable # Allow import, but disable doctests if we don't have matplotlib @@ -14,7 +15,9 @@ cm, have_matplotlib, _ = optional_package("matplotlib.cm") +@warn_on_args_to_kwargs() def colormap_lookup_table( + *, scale_range=(0, 1), hue_range=(0.8, 0), saturation_range=(1, 1), @@ -242,7 +245,8 @@ def orient2rgb(v): return orient -def line_colors(streamlines, cmap="rgb_standard"): +@warn_on_args_to_kwargs() +def line_colors(streamlines, *, cmap="rgb_standard"): """Create colors for streamlines to be used in actor.line. Parameters @@ -308,7 +312,8 @@ def simple_cmap(v): return simple_cmap -def create_colormap(v, name="plasma", auto=True): +@warn_on_args_to_kwargs() +def create_colormap(v, *, name="plasma", auto=True): """Create colors from a specific colormap and return it as an array of shape (N,3) where every row gives the corresponding r,g,b value. The colormaps we use are similar with those of matplotlib. @@ -511,7 +516,8 @@ def _lab2rgb(lab): return _xyz2rgb(tmp) -def distinguishable_colormap(bg=(0, 0, 0), exclude=None, nb_colors=None): +@warn_on_args_to_kwargs() +def distinguishable_colormap(*, bg=(0, 0, 0), exclude=None, nb_colors=None): """Generate colors that are maximally perceptually distinct. This function generates a set of colors which are distinguishable @@ -903,7 +909,8 @@ def get_xyz_coords(illuminant, observer): ) from err -def xyz2lab(xyz, illuminant="D65", observer="2"): +@warn_on_args_to_kwargs() +def xyz2lab(xyz, *, illuminant="D65", observer="2"): """XYZ to CIE-LAB color space conversion. Parameters @@ -950,7 +957,8 @@ def xyz2lab(xyz, illuminant="D65", observer="2"): return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) -def lab2xyz(lab, illuminant="D65", observer="2"): +@warn_on_args_to_kwargs() +def lab2xyz(lab, *, illuminant="D65", observer="2"): """CIE-LAB to XYZcolor space conversion. Parameters @@ -1001,7 +1009,8 @@ def lab2xyz(lab, illuminant="D65", observer="2"): return out -def rgb2lab(rgb, illuminant="D65", observer="2"): +@warn_on_args_to_kwargs() +def rgb2lab(rgb, *, illuminant="D65", observer="2"): """Conversion from the sRGB color space (IEC 61966-2-1:1999) to the CIE Lab colorspace under the given illuminant and observer. @@ -1028,10 +1037,11 @@ def rgb2lab(rgb, illuminant="D65", observer="2"): This implementation might have been modified. """ - return xyz2lab(rgb2xyz(rgb), illuminant, observer) + return xyz2lab(rgb2xyz(rgb), illuminant=illuminant, observer=observer) -def lab2rgb(lab, illuminant="D65", observer="2"): +@warn_on_args_to_kwargs() +def lab2rgb(lab, *, illuminant="D65", observer="2"): """Lab to RGB color space conversion. Parameters @@ -1057,4 +1067,4 @@ def lab2rgb(lab, illuminant="D65", observer="2"): This implementation might have been modified. """ - return xyz2rgb(lab2xyz(lab, illuminant, observer)) + return xyz2rgb(lab2xyz(lab, illuminant=illuminant, observer=observer)) diff --git a/fury/convert.py b/fury/convert.py index 25ad4d1a8..3945375d7 100644 --- a/fury/convert.py +++ b/fury/convert.py @@ -3,11 +3,13 @@ import numpy as np +from fury.decorators import warn_on_args_to_kwargs from fury.io import load_image +@warn_on_args_to_kwargs() def matplotlib_figure_to_numpy( - fig, dpi=100, fname=None, flip_up_down=True, transparent=False + fig, *, dpi=100, fname=None, flip_up_down=True, transparent=False ): """Convert a Matplotlib figure to a 3D numpy array with RGBA channels. @@ -54,7 +56,11 @@ def matplotlib_figure_to_numpy( arr = load_image(fname) else: fig.savefig( - fname, dpi=dpi, transparent=transparent, bbox_inches="tight", pad_inches=0 + fname, + dpi=dpi, + transparent=transparent, + bbox_inches="tight", + pad_inches=0, ) arr = load_image(fname) diff --git a/fury/decorators.py b/fury/decorators.py index 4cb1ee2cf..6ff1d3e19 100644 --- a/fury/decorators.py +++ b/fury/decorators.py @@ -1,8 +1,15 @@ """Decorators for FURY tests.""" +from functools import wraps +from inspect import signature import platform import re import sys +from warnings import warn + +from packaging import version + +import fury skip_linux = is_linux = platform.system().lower() == "linux" skip_osx = is_osx = platform.system().lower() == "darwin" @@ -43,3 +50,157 @@ def doctest_skip_parser(func): new_lines.append(code) func.__doc__ = "\n".join(new_lines) return func + + +def warn_on_args_to_kwargs( + from_version="0.1.0", + until_version="0.11.0", +): + """Decorator to enforce keyword-only arguments. + + This decorator enforces that all arguments after the first one are + keyword-only arguments. It also checks that all keyword arguments are + expected by the function. + + Parameters + ---------- + from_version: str, optional + The version of fury from which the function was supported. + until_version: str, optional + The version of fury until which the function was supported. + + Returns + ------- + decorator: Callable + Decorator function. + + Examples + -------- + >>> from fury.decorators import warn_on_args_to_kwargs + >>> import fury + >>> @warn_on_args_to_kwargs() + ... def f(a, b, *, c, d=1, e=1): + ... return a + b + c + d + e + >>> CURRENT_VERSION = fury.__version__ + >>> fury.__version__ = "0.11.0" + >>> f(1, 2, 3, 4, 5) + 15 + >>> f(1, 2, c=3, d=4, e=5) + 15 + >>> f(1, 2, 2, 4, e=5) + 14 + >>> f(1, 2, c=3, d=4) + 11 + >>> f(1, 2, d=3, e=5) + Traceback (most recent call last): + ... + TypeError: f() missing 1 required keyword-only argument: 'c' + >>> fury.__version__ = "0.12.0" + >>> f(1, 2, 3, 4, e=5) + Traceback (most recent call last): + ... + TypeError: f() takes 2 positional arguments but 4 positional arguments (and 1 keyword-only argument) were given + >>> fury.__version__ = CURRENT_VERSION + """ # noqa: E501 + + def decorator(func): + """Decorator function. This function enforces that all arguments after + the first one are keyword-only arguments. It also checks that all + keyword arguments are expected by the function. + + Parameters + ---------- + func: function + Function to be decorated. + + Returns + ------- + wrapper: Callable + Decorated function. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + sig = signature(func) + params = sig.parameters + # + KEYWORD_ONLY_ARGS = [ + arg.name for arg in params.values() if arg.kind == arg.KEYWORD_ONLY + ] + POSITIONAL_ARGS = [ + arg.name + for arg in params.values() + if arg.kind in (arg.POSITIONAL_OR_KEYWORD, arg.POSITIONAL_ONLY) + ] + + # Keyword-only arguments that do not have default values and not in kwargs + missing_kwargs = [ + arg + for arg in KEYWORD_ONLY_ARGS + if arg not in kwargs and params[arg].default == params[arg].empty + ] + + # Keyword-only arguments that have default values + ARG_DEFAULT = [ + arg + for arg in KEYWORD_ONLY_ARGS + if arg not in kwargs and params[arg].default != params[arg].empty + ] + func_params_sample = [] + + # Create a sample of the function parameters + for arg in params.values(): + if arg.kind in (arg.POSITIONAL_OR_KEYWORD, arg.POSITIONAL_ONLY): + func_params_sample.append(f"{arg.name}_value") + elif arg.kind == arg.KEYWORD_ONLY: + func_params_sample.append(f"{arg.name}='value'") + func_params_sample = ", ".join(func_params_sample) + args_kwargs_len = len(args) + len(kwargs) + params_len = len(params) + try: + return func(*args, **kwargs) + except TypeError as e: + FURY_CURRENT_VERSION = fury.__version__ + + if ARG_DEFAULT: + missing_kwargs += ARG_DEFAULT + if missing_kwargs and params_len >= args_kwargs_len: + # if the version of fury is greater than until_version, + # an error should be displayed, + if version.parse(FURY_CURRENT_VERSION) > version.parse( + until_version + ): + raise TypeError(e) from e + + positional_args_len = len(POSITIONAL_ARGS) + args_k = list(args[positional_args_len:]) + args = list(args[:positional_args_len]) + kwargs.update(dict(zip(missing_kwargs, args_k))) + result = func(*args, **kwargs) + + # if from_version is less or equal to fury.__version__ and, + # less or equal to until_version + # a warning should be displayed. + if ( + version.parse(from_version) + <= version.parse(FURY_CURRENT_VERSION) + <= version.parse(until_version) + ): + warn( + f"We'll no longer accept the way you call the " + f"{func.__name__} function in future versions of FURY.\n\n" + "Here's how to call the Function {}: {}({})\n".format( + func.__name__, func.__name__, func_params_sample + ), + UserWarning, + stacklevel=3, + ) + + # if the current version of fury is less than from_version, + # the function should be called without any changes. + + return result + + return wrapper + + return decorator diff --git a/fury/gltf.py b/fury/gltf.py index 5b02ff68d..962515ab9 100644 --- a/fury/gltf.py +++ b/fury/gltf.py @@ -17,6 +17,7 @@ step_interpolator, tan_cubic_spline_interpolator, ) +from fury.decorators import warn_on_args_to_kwargs from fury.lib import Camera, Matrix4x4, Texture, Transform, numpy_support comp_type = { @@ -32,7 +33,8 @@ class glTF: - def __init__(self, filename, apply_normals=False): + @warn_on_args_to_kwargs() + def __init__(self, filename, *, apply_normals=False): """Read and generate actors from glTF files. Parameters @@ -87,7 +89,7 @@ def __init__(self, filename, apply_normals=False): self.morph_vertices = [] self.morph_weights = [] - self.inspect_scene(0) + self.inspect_scene(scene_id=0) self._actors = [] self._bactors = {} @@ -121,7 +123,8 @@ def actors(self): return self._actors - def inspect_scene(self, scene_id=0): + @warn_on_args_to_kwargs() + def inspect_scene(self, *, scene_id=0): """Loop over nodes in a scene. Parameters @@ -138,7 +141,8 @@ def inspect_scene(self, scene_id=0): for i, animation in enumerate(self.gltf.animations): self.transverse_channels(animation, i) - def transverse_node(self, nextnode_id, matrix, parent=None, is_joint=False): + @warn_on_args_to_kwargs() + def transverse_node(self, nextnode_id, matrix, *, parent=None, is_joint=False): """Load mesh and generates transformation matrix. Parameters @@ -204,7 +208,9 @@ def transverse_node(self, nextnode_id, matrix, parent=None, is_joint=False): for bone, ibm in zip(joints, ibms): self.bones.append(bone) self.ibms[bone] = ibm - self.transverse_node(joints[0], np.identity(4), parent, is_joint=True) + self.transverse_node( + joints[0], np.identity(4), parent=parent, is_joint=True + ) if node.camera is not None: camera_id = node.camera @@ -212,7 +218,12 @@ def transverse_node(self, nextnode_id, matrix, parent=None, is_joint=False): if node.children: for child_id in node.children: - self.transverse_node(child_id, next_matrix, parent, is_joint) + self.transverse_node( + child_id, + next_matrix, + parent=parent, + is_joint=is_joint, + ) def load_mesh(self, mesh_id, transform_mat, parent): """Load the mesh data from accessor and applies the transformation. @@ -645,12 +656,14 @@ def generate_tmatrix(self, transf, prop): matrix = transf return matrix + @warn_on_args_to_kwargs() def transverse_animations( self, animation, bone_id, timestamp, joint_matrices, + *, parent_bone_deform=None, ): """Calculate skinning matrix (Joint Matrices) and transform bone for @@ -696,7 +709,11 @@ def transverse_animations( c_bones = node.children for c_anim, c_bone in zip(c_animations, c_bones): self.transverse_animations( - c_anim, c_bone, timestamp, joint_matrices, new_deform + c_anim, + c_bone, + timestamp, + joint_matrices, + parent_bone_deform=new_deform, ) def update_skin(self, animation): @@ -722,16 +739,25 @@ def update_skin(self, animation): parent_transform = np.identity(4) for child in _animation.child_animations: self.transverse_animations( - child, self.bones[0], timestamp, joint_matrices, parent_transform + child, + self.bones[0], + timestamp, + joint_matrices, + parent_bone_deform=parent_transform, ) for i, vertex in enumerate(self._vertices): - vertex[:] = self.apply_skin_matrix(self._vcopy[i], joint_matrices, i) + vertex[:] = self.apply_skin_matrix( + self._vcopy[i], + joint_matrices, + actor_index=i, + ) actor_transf = self.transformations[i] vertex[:] = transform.apply_transformation(vertex, actor_transf) utils.update_actor(self._actors[i]) utils.compute_bounds(self._actors[i]) - def initialize_skin(self, animation, bones=False, length=0.2): + @warn_on_args_to_kwargs() + def initialize_skin(self, animation, *, bones=False, length=0.2): """Create bones and add to the animation and initialise `update_skin` Parameters @@ -748,11 +774,12 @@ def initialize_skin(self, animation, bones=False, length=0.2): """ self.show_bones = bones if bones: - self.get_joint_actors(length, False) + self.get_joint_actors(length=length, with_transforms=False) animation.add_actor(list(self._bactors.values())) self.update_skin(animation) - def apply_skin_matrix(self, vertices, joint_matrices, actor_index=0): + @warn_on_args_to_kwargs() + def apply_skin_matrix(self, vertices, joint_matrices, *, actor_index=0): """Apply the skinnig matrix, that transform the vertices. Parameters @@ -845,7 +872,8 @@ def skin_animation(self): root_animation.add_actor(self._actors) return root_animations - def get_joint_actors(self, length=0.5, with_transforms=False): + @warn_on_args_to_kwargs() + def get_joint_actors(self, *, length=0.5, with_transforms=False): """Create an arrow actor for each bone in a skinned model. Parameters @@ -1039,7 +1067,8 @@ def main_animation(self): return main_animation -def export_scene(scene, filename="default.gltf"): +@warn_on_args_to_kwargs() +def export_scene(scene, *, filename="default.gltf"): """Generate gltf from FURY scene. Parameters @@ -1175,8 +1204,8 @@ def _connect_primitives(gltf, actor, buff_file, byteoffset, count, name): gltflib.FLOAT, len(vertices) // atype, gltflib.VEC3, - amax, - amin, + max=amax, + min=amin, ) byteoffset += blength vertex = count @@ -1200,8 +1229,8 @@ def _connect_primitives(gltf, actor, buff_file, byteoffset, count, name): gltflib.FLOAT, len(normals) // atype, gltflib.VEC3, - amax, - amin, + max=amax, + min=amin, ) byteoffset += blength normal = count @@ -1251,7 +1280,7 @@ def _connect_primitives(gltf, actor, buff_file, byteoffset, count, name): color = count count += 1 material = None if tcoords is None else 0 - prim = get_prim(vertex, index, color, tcoord, normal, material, mode) + prim = get_prim(vertex, index, color, tcoord, normal, material, mode=mode) return prim, byteoffset, count @@ -1271,7 +1300,8 @@ def write_scene(gltf, nodes): gltf.scenes.append(scene) -def write_node(gltf, mesh_id=None, camera_id=None): +@warn_on_args_to_kwargs() +def write_node(gltf, *, mesh_id=None, camera_id=None): """Create node Parameters @@ -1339,7 +1369,8 @@ def write_camera(gltf, camera): gltf.cameras.append(cam) -def get_prim(vertex, index, color, tcoord, normal, material, mode=4): +@warn_on_args_to_kwargs() +def get_prim(vertex, index, color, tcoord, normal, material, *, mode=4): """Return a Primitive object. Parameters @@ -1409,8 +1440,9 @@ def write_material(gltf, basecolortexture: int, uri: str): gltf.images.append(image) +@warn_on_args_to_kwargs() def write_accessor( - gltf, bufferview, byte_offset, comp_type, count, accssor_type, max=None, min=None + gltf, bufferview, byte_offset, comp_type, count, accssor_type, *, max=None, min=None ): """Write accessor in the gltf. @@ -1447,7 +1479,8 @@ def write_accessor( gltf.accessors.append(accessor) -def write_bufferview(gltf, buffer, byte_offset, byte_length, byte_stride=None): +@warn_on_args_to_kwargs() +def write_bufferview(gltf, buffer, byte_offset, byte_length, *, byte_stride=None): """Write bufferview in the gltf. Parameters diff --git a/fury/interactor.py b/fury/interactor.py index 399f44992..febe0d610 100644 --- a/fury/interactor.py +++ b/fury/interactor.py @@ -4,6 +4,7 @@ import numpy as np +from fury.decorators import warn_on_args_to_kwargs from fury.lib import ( Command, InteractorStyle, @@ -173,7 +174,8 @@ def _process_event(self, obj, evt): self.event.reset() # Event fully processed. - def _button_clicked(self, button, last_event=-1, before_last_event=-2): + @warn_on_args_to_kwargs() + def _button_clicked(self, button, *, last_event=-1, before_last_event=-2): if len(self.history) < abs(before_last_event): return False @@ -186,7 +188,10 @@ def _button_clicked(self, button, last_event=-1, before_last_event=-2): return True def _button_double_clicked(self, button): - if not (self._button_clicked(button) and self._button_clicked(button, -3, -4)): + if not ( + self._button_clicked(button) + and self._button_clicked(button, last_event=-3, before_last_event=-4) + ): return False return True @@ -383,7 +388,8 @@ def force_render(self): """Causes the scene to refresh.""" self.GetInteractor().GetRenderWindow().Render() - def add_callback(self, prop, event_type, callback, priority=0, args=None): + @warn_on_args_to_kwargs() + def add_callback(self, prop, event_type, callback, *, priority=0, args=None): """Add a callback associated to a specific event for a VTK prop. Parameters diff --git a/fury/io.py b/fury/io.py index 828002c2d..8f2900014 100644 --- a/fury/io.py +++ b/fury/io.py @@ -6,6 +6,7 @@ from PIL import Image import numpy as np +from fury.decorators import warn_on_args_to_kwargs from fury.lib import ( BMPReader, BMPWriter, @@ -34,7 +35,8 @@ from fury.utils import set_input -def load_cubemap_texture(fnames, interpolate_on=True, mipmap_on=True): +@warn_on_args_to_kwargs() +def load_cubemap_texture(fnames, *, interpolate_on=True, mipmap_on=True): """Load a cube map texture from a list of 6 images. Parameters @@ -74,7 +76,8 @@ def load_cubemap_texture(fnames, interpolate_on=True, mipmap_on=True): return texture -def load_image(filename, as_vtktype=False, use_pillow=True): +@warn_on_args_to_kwargs() +def load_image(filename, *, as_vtktype=False, use_pillow=True): """Load an image. Parameters @@ -92,8 +95,8 @@ def load_image(filename, as_vtktype=False, use_pillow=True): desired image array """ - is_url = filename.lower().startswith("http://") or filename.lower().startswith( - "https://" + is_url = (filename.lower().startswith("http://")) or ( + filename.lower().startswith("https://") ) if is_url: @@ -135,7 +138,14 @@ def load_image(filename, as_vtktype=False, use_pillow=True): # width, height vtk_image.SetDimensions(image.shape[1], image.shape[0], depth) - vtk_image.SetExtent(0, image.shape[1] - 1, 0, image.shape[0] - 1, 0, 0) + vtk_image.SetExtent( + 0, + image.shape[1] - 1, + 0, + image.shape[0] - 1, + 0, + 0, + ) vtk_image.SetSpacing(1.0, 1.0, 1.0) vtk_image.SetOrigin(0.0, 0.0, 0.0) @@ -210,9 +220,11 @@ def load_text(file): return text +@warn_on_args_to_kwargs() def save_image( arr, filename, + *, compression_quality=75, compression_type="deflation", use_pillow=True, @@ -307,7 +319,13 @@ def save_image( writer.SetQuality(compression_quality) if extension.lower() in [".tif", ".tiff"]: compression_type = compression_type or "nocompression" - l_compression = ["nocompression", "packbits", "jpeg", "deflate", "lzw"] + l_compression = [ + "nocompression", + "packbits", + "jpeg", + "deflate", + "lzw", + ] if compression_type.lower() in l_compression: comp_id = l_compression.index(compression_type.lower()) @@ -363,7 +381,8 @@ def load_polydata(file_name): return reader.GetOutput() -def save_polydata(polydata, file_name, binary=False, color_array_name=None): +@warn_on_args_to_kwargs() +def save_polydata(polydata, file_name, *, binary=False, color_array_name=None): """Save a vtk polydata to a supported format file. Save formats can be VTK, FIB, PLY, STL and XML. @@ -413,7 +432,8 @@ def save_polydata(polydata, file_name, binary=False, color_array_name=None): writer.Write() -def load_sprite_sheet(sheet_path, nb_rows, nb_cols, as_vtktype=False): +@warn_on_args_to_kwargs() +def load_sprite_sheet(sheet_path, nb_rows, nb_cols, *, as_vtktype=False): """Process and load sprites from a sprite sheet. Parameters @@ -450,13 +470,18 @@ def load_sprite_sheet(sheet_path, nb_rows, nb_cols, as_vtktype=False): nxt_col * sprite_size_y, ) - sprite_arr = sprite_sheet[box[0] : box[2], box[1] : box[3]] + sprite_arr = sprite_sheet[ + box[0] : box[2], box[1] : box[3] # noqa: E203 + ] if as_vtktype: with InTemporaryDirectory() as tdir: tmp_img_path = os.path.join(tdir, f"{row}{col}.png") save_image(sprite_arr, tmp_img_path, compression_quality=100) - sprite_dicts[(row, col)] = load_image(tmp_img_path, as_vtktype=True) + sprite_dicts[(row, col)] = load_image( + tmp_img_path, + as_vtktype=True, + ) else: sprite_dicts[(row, col)] = sprite_arr diff --git a/fury/layout.py b/fury/layout.py index 7647949b5..f227b99cd 100644 --- a/fury/layout.py +++ b/fury/layout.py @@ -2,6 +2,7 @@ import numpy as np +from fury.decorators import warn_on_args_to_kwargs from fury.utils import get_bounding_box_sizes, get_grid_cells_position, is_ui @@ -32,8 +33,10 @@ class GridLayout(Layout): """ + @warn_on_args_to_kwargs() def __init__( self, + *, cell_padding=0, cell_shape="rect", aspect_ratio=16 / 9.0, @@ -88,11 +91,25 @@ def get_cells_shape(self, actors): """ if self.cell_shape == "rect": - bounding_box_sizes = np.asarray(list(map(self.compute_sizes, actors))) + bounding_box_sizes = np.asarray( + list( + map( + self.compute_sizes, + actors, + ) + ) + ) cell_shape = np.max(bounding_box_sizes, axis=0)[:2] shapes = [cell_shape] * len(actors) elif self.cell_shape == "square": - bounding_box_sizes = np.asarray(list(map(self.compute_sizes, actors))) + bounding_box_sizes = np.asarray( + list( + map( + self.compute_sizes, + actors, + ) + ) + ) cell_shape = np.max(bounding_box_sizes, axis=0)[:2] shapes = [(max(cell_shape),) * 2] * len(actors) elif self.cell_shape == "diagonal": @@ -110,7 +127,11 @@ def get_cells_shape(self, actors): longest_diagonal = np.max(diagonals) shapes = [(longest_diagonal, longest_diagonal)] * len(actors) else: - raise ValueError("Unknown cell shape: '{0}'".format(self.cell_shape)) + raise ValueError( + "Unknown cell shape: '{0}'".format( + self.cell_shape, + ) + ) return shapes @@ -133,7 +154,11 @@ def compute_positions(self, actors): # Add padding, if any, around every cell. shapes = [np.array(self.cell_padding) / 2.0 + s for s in shapes] - positions = get_grid_cells_position(shapes, self.aspect_ratio, self.dim) + positions = get_grid_cells_position( + shapes, + aspect_ratio=self.aspect_ratio, + dim=self.dim, + ) positions += self.position_offset return positions @@ -161,7 +186,8 @@ def compute_sizes(self, actor): class HorizontalLayout(GridLayout): """Provide functionalities for laying out actors in a horizontal layout.""" - def __init__(self, cell_padding=0, cell_shape="rect"): + @warn_on_args_to_kwargs() + def __init__(self, *, cell_padding=0, cell_shape="rect"): """Initialize the Horizontal layout. Parameters @@ -216,7 +242,8 @@ def compute_positions(self, actors): class VerticalLayout(GridLayout): """Provide functionalities for laying out actors in a vertical stack.""" - def __init__(self, cell_padding=0, cell_shape="rect"): + @warn_on_args_to_kwargs() + def __init__(self, *, cell_padding=0, cell_shape="rect"): """Initialize the Vertical layout. Parameters @@ -270,7 +297,8 @@ def compute_positions(self, actors): class XLayout(HorizontalLayout): """Provide functionalities for laying out actors along x-axis.""" - def __init__(self, direction="x+", cell_padding=0, cell_shape="rect"): + @warn_on_args_to_kwargs() + def __init__(self, *, direction="x+", cell_padding=0, cell_shape="rect"): """Initialize the X layout. Parameters @@ -297,7 +325,10 @@ def __init__(self, direction="x+", cell_padding=0, cell_shape="rect"): if self.direction not in ["x+", "x-"]: raise ValueError(f"{direction} is not a valid direction") - super(XLayout, self).__init__(cell_padding=cell_padding, cell_shape=cell_shape) + super(XLayout, self).__init__( + cell_padding=cell_padding, + cell_shape=cell_shape, + ) def get_cells_shape(self, actors): """Get the 2D shape (on the xy-plane) of some actors according to @@ -352,7 +383,8 @@ def apply(self, actors): class YLayout(VerticalLayout): """Provide functionalities for laying out actors along y-axis.""" - def __init__(self, direction="y+", cell_padding=0, cell_shape="rect"): + @warn_on_args_to_kwargs() + def __init__(self, *, direction="y+", cell_padding=0, cell_shape="rect"): """Initialize the Y layout. Parameters @@ -379,7 +411,10 @@ def __init__(self, direction="y+", cell_padding=0, cell_shape="rect"): if self.direction not in ["y+", "y-"]: raise ValueError(f"{direction} is not a valid direction") - super(YLayout, self).__init__(cell_padding=cell_padding, cell_shape=cell_shape) + super(YLayout, self).__init__( + cell_padding=cell_padding, + cell_shape=cell_shape, + ) def get_cells_shape(self, actors): """Get the 2D shape (on the xy-plane) of some actors according to @@ -434,7 +469,8 @@ def apply(self, actors): class ZLayout(GridLayout): """Provide functionalities for laying out actors along z-axis.""" - def __init__(self, direction="z+", cell_padding=0, cell_shape="rect"): + @warn_on_args_to_kwargs() + def __init__(self, *, direction="z+", cell_padding=0, cell_shape="rect"): """Initialize the Z layout. Parameters @@ -461,7 +497,10 @@ def __init__(self, direction="z+", cell_padding=0, cell_shape="rect"): if self.direction not in ["z+", "z-"]: raise ValueError(f"{direction} is not a valid direction") - super(ZLayout, self).__init__(cell_padding=cell_padding, cell_shape=cell_shape) + super(ZLayout, self).__init__( + cell_padding=cell_padding, + cell_shape=cell_shape, + ) def get_cells_shape(self, actors): """Get the shape (on the z-plane) of some actors according to @@ -482,7 +521,14 @@ def get_cells_shape(self, actors): actors = actors[::-1] if self.cell_shape == "rect" or self.cell_shape == "square": - bounding_box_sizes = np.asarray(list(map(get_bounding_box_sizes, actors))) + bounding_box_sizes = np.asarray( + list( + map( + get_bounding_box_sizes, + actors, + ) + ) + ) cell_shape = np.max(bounding_box_sizes, axis=0)[2] shapes = [cell_shape] * len(actors) elif self.cell_shape == "diagonal": @@ -491,7 +537,11 @@ def get_cells_shape(self, actors): longest_diagonal = np.max([a.GetLength() for a in actors]) shapes = [longest_diagonal] * len(actors) else: - raise ValueError("Unknown cell shape: '{0}'".format(self.cell_shape)) + raise ValueError( + "Unknown cell shape: '{0}'".format( + self.cell_shape, + ) + ) return shapes diff --git a/fury/material.py b/fury/material.py index 22891395a..98b97eae4 100644 --- a/fury/material.py +++ b/fury/material.py @@ -1,6 +1,7 @@ import os import warnings +from fury.decorators import warn_on_args_to_kwargs from fury.lib import VTK_OBJECT, calldata_type from fury.shaders import ( add_shader_callback, @@ -134,8 +135,10 @@ def coat_ior(self, coat_ior): self.__actor_properties.SetCoatIOR(coat_ior) +@warn_on_args_to_kwargs() def manifest_pbr( actor, + *, metallic=0, roughness=0.5, anisotropy=0, @@ -208,8 +211,10 @@ def manifest_pbr( return None +@warn_on_args_to_kwargs() def manifest_principled( actor, + *, subsurface=0, metallic=0, specular=0, @@ -284,20 +289,40 @@ def manifest_principled( @calldata_type(VTK_OBJECT) def uniforms_callback(_caller, _event, calldata=None): if calldata is not None: - calldata.SetUniformf("subsurface", principled_params["subsurface"]) + calldata.SetUniformf( + "subsurface", + principled_params["subsurface"], + ) calldata.SetUniformf("metallic", principled_params["metallic"]) - calldata.SetUniformf("specularTint", principled_params["specular_tint"]) - calldata.SetUniformf("roughness", principled_params["roughness"]) - calldata.SetUniformf("anisotropic", principled_params["anisotropic"]) + calldata.SetUniformf( + "specularTint", + principled_params["specular_tint"], + ) + calldata.SetUniformf( + "roughness", + principled_params["roughness"], + ) + calldata.SetUniformf( + "anisotropic", + principled_params["anisotropic"], + ) calldata.SetUniformf("sheen", principled_params["sheen"]) - calldata.SetUniformf("sheenTint", principled_params["sheen_tint"]) - calldata.SetUniformf("clearcoat", principled_params["clearcoat"]) calldata.SetUniformf( - "clearcoatGloss", principled_params["clearcoat_gloss"] + "sheenTint", + principled_params["sheen_tint"], + ) + calldata.SetUniformf( + "clearcoat", + principled_params["clearcoat"], + ) + calldata.SetUniformf( + "clearcoatGloss", + principled_params["clearcoat_gloss"], ) calldata.SetUniform3f( - "anisotropicDirection", principled_params["anisotropic_direction"] + "anisotropicDirection", + principled_params["anisotropic_direction"], ) add_shader_callback(actor, uniforms_callback) @@ -374,7 +399,13 @@ def uniforms_callback(_caller, _event, calldata=None): # Importing Geometry Shadowing and Masking Function (GF): Smith Ground # Glass Unknown (G_{GGX}) needed for the Isotropic Specular and Clear # Coat lobes - smith_ggx = import_fury_shader(os.path.join("lighting", "gf", "smith_ggx.frag")) + smith_ggx = import_fury_shader( + os.path.join( + "lighting", + "gf", + "smith_ggx.frag", + ) + ) # Importing Geometry Shadowing and Masking Function (GF): Anisotropic # form of the Smith Ground Glass Unknown (G_{GGXanisotropic}) needed @@ -390,7 +421,13 @@ def uniforms_callback(_caller, _event, calldata=None): subsurface = import_fury_shader( os.path.join("lighting", "principled", "subsurface.frag") ) - sheen = import_fury_shader(os.path.join("lighting", "principled", "sheen.frag")) + sheen = import_fury_shader( + os.path.join( + "lighting", + "principled", + "sheen.frag", + ) + ) specular_isotropic = import_fury_shader( os.path.join("lighting", "principled", "specular_isotropic.frag") ) @@ -574,8 +611,10 @@ def uniforms_callback(_caller, _event, calldata=None): return None +@warn_on_args_to_kwargs() def manifest_standard( actor, + *, ambient_level=0, ambient_color=(1, 1, 1), diffuse_level=1, diff --git a/fury/molecular.py b/fury/molecular.py index 394591297..ea0e13b52 100644 --- a/fury/molecular.py +++ b/fury/molecular.py @@ -5,6 +5,7 @@ import numpy as np from fury.actor import streamtube +from fury.decorators import warn_on_args_to_kwargs from fury.lib import ( VTK_FLOAT, VTK_ID_TYPE, @@ -34,8 +35,10 @@ class Molecule(Mol): This is a more pythonic version of ``Molecule``. """ + @warn_on_args_to_kwargs() def __init__( self, + *, atomic_numbers=None, coords=None, atom_names=None, @@ -107,7 +110,10 @@ def __init__( self.helix = helix self.is_hetatm = is_hetatm coords = numpy_to_vtk_points(coords) - atom_nums = nps.numpy_to_vtk(atomic_numbers, array_type=VTK_UNSIGNED_SHORT) + atom_nums = nps.numpy_to_vtk( + atomic_numbers, + array_type=VTK_UNSIGNED_SHORT, + ) atom_nums.SetName("Atomic Numbers") fieldData = DataSetAttributes() fieldData.AddArray(atom_nums) @@ -151,7 +157,8 @@ def add_atom(molecule, atomic_num, x_coord, y_coord, z_coord): molecule.AppendAtom(atomic_num, x_coord, y_coord, z_coord) -def add_bond(molecule, atom1_index, atom2_index, bond_order=1): +@warn_on_args_to_kwargs() +def add_bond(molecule, atom1_index, atom2_index, *, bond_order=1): """Add bonding data to our molecule. Establish a bond of type bond_order between the atom at atom1_index and the atom at atom2_index. @@ -413,7 +420,8 @@ def atomic_number(self, element_name): """ return self.GetAtomicNumber(element_name) - def atomic_radius(self, atomic_number, radius_type="VDW"): + @warn_on_args_to_kwargs() + def atomic_radius(self, atomic_number, *, radius_type="VDW"): """Given an atomic number, return either the covalent radius of the atom (in Å) or return the Van Der Waals radius (in Å) of the atom depending on radius_type. @@ -457,7 +465,8 @@ def atom_color(self, atomic_number): return rgb -def sphere_cpk(molecule, colormode="discrete"): +@warn_on_args_to_kwargs() +def sphere_cpk(molecule, *, colormode="discrete"): """Create an actor for sphere molecular representation. It's also referred to as CPK model and space-filling model. @@ -502,7 +511,10 @@ def sphere_cpk(molecule, colormode="discrete"): msp_mapper.SetAtomColorMode(0) else: msp_mapper.SetAtomColorMode(1) - warnings.warn("Incorrect colormode specified! Using discrete.", stacklevel=2) + warnings.warn( + "Incorrect colormode specified! Using discrete.", + stacklevel=2, + ) # To-Do manipulate shading properties to make it look aesthetic molecule_actor = Actor() @@ -510,8 +522,10 @@ def sphere_cpk(molecule, colormode="discrete"): return molecule_actor +@warn_on_args_to_kwargs() def ball_stick( molecule, + *, colormode="discrete", atom_scale_factor=0.3, bond_thickness=0.1, @@ -586,13 +600,17 @@ def ball_stick( bs_mapper.SetBondColorMode(0) else: bs_mapper.SetAtomColorMode(1) - warnings.warn("Incorrect colormode specified! Using discrete.", stacklevel=2) + warnings.warn( + "Incorrect colormode specified! Using discrete.", + stacklevel=2, + ) molecule_actor = Actor() molecule_actor.SetMapper(bs_mapper) return molecule_actor -def stick(molecule, colormode="discrete", bond_thickness=0.1): +@warn_on_args_to_kwargs() +def stick(molecule, *, colormode="discrete", bond_thickness=0.1): """Create an actor for stick molecular representation. Parameters @@ -623,7 +641,10 @@ def stick(molecule, colormode="discrete", bond_thickness=0.1): """ if molecule.total_num_bonds == 0: raise ValueError( - "No bonding data available for the molecule! Stick " "model cannot be made!" + ( + "No bonding data available for the molecule! Stick " + "model cannot be made!" + ) ) colormode = colormode.lower() mst_mapper = OpenGLMoleculeMapper() @@ -641,7 +662,10 @@ def stick(molecule, colormode="discrete", bond_thickness=0.1): mst_mapper.SetBondColorMode(0) else: mst_mapper.SetAtomColorMode(1) - warnings.warn("Incorrect colormode specified! Using discrete.", stacklevel=2) + warnings.warn( + "Incorrect colormode specified! Using discrete.", + stacklevel=2, + ) molecule_actor = Actor() molecule_actor.SetMapper(mst_mapper) return molecule_actor @@ -677,13 +701,21 @@ def ribbon(molecule): resi = molecule.residue_seq[i] for j, _ in enumerate(molecule.sheet): sheet = molecule.sheet[j] - if molecule.chain[i] != sheet[0] or resi < sheet[1] or resi > sheet[3]: + if ( + (molecule.chain[i] != sheet[0]) + or (resi < sheet[1]) + or (resi > sheet[3]) + ): continue secondary_structures[i] = ord("s") for j, _ in enumerate(molecule.helix): helix = molecule.helix[j] - if molecule.chain[i] != helix[0] or resi < helix[1] or resi > helix[3]: + if ( + (molecule.chain[i] != helix[0]) + or (resi < helix[1]) + or (resi > helix[3]) + ): continue secondary_structures[i] = ord("h") @@ -735,13 +767,21 @@ def ribbon(molecule): # for secondary structures begin newarr = np.ones(num_total_atoms) - s_sb = nps.numpy_to_vtk(num_array=newarr, deep=True, array_type=VTK_UNSIGNED_CHAR) + s_sb = nps.numpy_to_vtk( + num_array=newarr, + deep=True, + array_type=VTK_UNSIGNED_CHAR, + ) s_sb.SetName("secondary_structures_begin") output.GetPointData().AddArray(s_sb) # for secondary structures end newarr = np.ones(num_total_atoms) - s_se = nps.numpy_to_vtk(num_array=newarr, deep=True, array_type=VTK_UNSIGNED_CHAR) + s_se = nps.numpy_to_vtk( + num_array=newarr, + deep=True, + array_type=VTK_UNSIGNED_CHAR, + ) s_se.SetName("secondary_structures_end") output.GetPointData().AddArray(s_se) @@ -766,10 +806,17 @@ def ribbon(molecule): rgb = np.ones((num_total_atoms, 3)) for i in range(num_total_atoms): - radii[i] = np.repeat(table.atomic_radius(all_atomic_numbers[i], "VDW"), 3) + radii[i] = np.repeat( + table.atomic_radius(all_atomic_numbers[i], radius_type="VDW"), + 3, + ) rgb[i] = table.atom_color(all_atomic_numbers[i]) - Rgb = nps.numpy_to_vtk(num_array=rgb, deep=True, array_type=VTK_UNSIGNED_CHAR) + Rgb = nps.numpy_to_vtk( + num_array=rgb, + deep=True, + array_type=VTK_UNSIGNED_CHAR, + ) Rgb.SetName("rgb_colors") output.GetPointData().SetScalars(Rgb) @@ -792,7 +839,8 @@ def ribbon(molecule): return molecule_actor -def bounding_box(molecule, colors=(1, 1, 1), linewidth=0.3): +@warn_on_args_to_kwargs() +def bounding_box(molecule, *, colors=(1, 1, 1), linewidth=0.3): """Create a bounding box for a molecule. Parameters diff --git a/fury/optpkg.py b/fury/optpkg.py index 053b62c6b..763ec64b2 100644 --- a/fury/optpkg.py +++ b/fury/optpkg.py @@ -2,6 +2,8 @@ import importlib +from fury.decorators import warn_on_args_to_kwargs + try: import pytest except ImportError: @@ -46,7 +48,7 @@ class TripWire: ... except ImportError: ... silly_module_name = TripWire('We do not have silly_module_name') >>> msg = 'with silly string' - >>> silly_module_name.do_silly_thing(msg) #doctest: +IGNORE_EXCEPTION_DETAIL + >>> silly_module_name.do_silly_thing(msg) #doctest: +IGNORE_EXCEPTION_DETAIL # noqa Traceback (most recent call last): ... TripWireError: We do not have silly_module_name @@ -65,7 +67,8 @@ def __call__(self, *args, **kwargs): raise TripWireError(self._msg) -def optional_package(name, trip_msg=None): +@warn_on_args_to_kwargs() +def optional_package(name, *, trip_msg=None): """Return package-like thing and module setup for package `name`. Parameters diff --git a/fury/pick.py b/fury/pick.py index 6949638c5..49941849a 100644 --- a/fury/pick.py +++ b/fury/pick.py @@ -2,6 +2,7 @@ import numpy as np +from fury.decorators import warn_on_args_to_kwargs from fury.lib import ( CellPicker, DataObject, @@ -16,7 +17,15 @@ class PickingManager: """Picking Manager helps with picking 3D objects.""" - def __init__(self, vertices=True, faces=True, actors=True, world_coords=True): + @warn_on_args_to_kwargs() + def __init__( + self, + *, + vertices=True, + faces=True, + actors=True, + world_coords=True, + ): """Initialize Picking Manager. Parameters @@ -120,7 +129,8 @@ def pickable_off(self, actors): class SelectionManager: """Selection Manager helps with picking many objects simultaneously.""" - def __init__(self, select="faces"): + @warn_on_args_to_kwargs() + def __init__(self, *, select="faces"): """Initialize Selection Manager. Parameters @@ -171,7 +181,8 @@ def pick(self, disp_xy, sc): """ return self.select(disp_xy, sc, area=0)[0] - def select(self, disp_xy, sc, area=0): + @warn_on_args_to_kwargs() + def select(self, disp_xy, sc, *, area=0): """Select multiple objects using display coordinates. Parameters @@ -202,26 +213,49 @@ def select(self, disp_xy, sc, area=0): res = self.hsel.Select() except OverflowError: - return {0: {"node": None, "vertex": None, "face": None, "actor": None}} + return { + 0: { + "node": None, + "vertex": None, + "face": None, + "actor": None, + } + } num_nodes = res.GetNumberOfNodes() if num_nodes < 1: sel_node = None - return {0: {"node": None, "vertex": None, "face": None, "actor": None}} + return { + 0: { + "node": None, + "vertex": None, + "face": None, + "actor": None, + } + } else: for i in range(num_nodes): sel_node = res.GetNode(i) - info = {"node": None, "vertex": None, "face": None, "actor": None} + info = { + "node": None, + "vertex": None, + "face": None, + "actor": None, + } if sel_node is not None: selected_nodes = set( np.floor( - numpy_support.vtk_to_numpy(sel_node.GetSelectionList()) + numpy_support.vtk_to_numpy( + sel_node.GetSelectionList(), + ) ).astype(int) ) info["node"] = sel_node - info["actor"] = sel_node.GetProperties().Get(sel_node.PROP()) + info["actor"] = sel_node.GetProperties().Get( + sel_node.PROP(), + ) if self.selected_type == "faces": info["face"] = list(selected_nodes) if self.selected_type == "vertex": diff --git a/fury/pkg_info.py b/fury/pkg_info.py index 7bd7da41e..3773fbf22 100644 --- a/fury/pkg_info.py +++ b/fury/pkg_info.py @@ -4,6 +4,8 @@ from packaging.version import Version +from fury.decorators import warn_on_args_to_kwargs + try: from ._version import __version__ except ImportError: @@ -12,6 +14,7 @@ COMMIT_HASH = "$Format:%h$" +@warn_on_args_to_kwargs() def pkg_commit_hash(pkg_path: str | None = None) -> tuple[str, str]: """Get short form of commit hash diff --git a/fury/primitive.py b/fury/primitive.py index 768a92626..67a94663d 100644 --- a/fury/primitive.py +++ b/fury/primitive.py @@ -9,6 +9,7 @@ from scipy.version import short_version from fury.data import DATA_DIR +from fury.decorators import warn_on_args_to_kwargs from fury.transform import cart2sphere, sphere2cart from fury.utils import fix_winding_order @@ -46,8 +47,9 @@ def faces_from_sphere_vertices(vertices): return faces +@warn_on_args_to_kwargs() def repeat_primitive_function( - func, centers, func_args=None, directions=(1, 0, 0), colors=(1, 0, 0), scales=1 + func, centers, *, func_args=None, directions=(1, 0, 0), colors=(1, 0, 0), scales=1 ): """Repeat Vertices and triangles of a specific primitive function. @@ -91,12 +93,11 @@ def repeat_primitive_function( "sq_params should 1 or equal to the numbers \ of centers" ) - vertices = np.concatenate([func(i)[0] for i in func_args]) return repeat_primitive( - vertices=vertices, - faces=faces, - centers=centers, + vertices, + faces, + centers, directions=directions, colors=colors, scales=scales, @@ -104,10 +105,12 @@ def repeat_primitive_function( ) +@warn_on_args_to_kwargs() def repeat_primitive( vertices, faces, centers, + *, directions=None, colors=(1, 0, 0), scales=1, @@ -173,7 +176,8 @@ def repeat_primitive( axis=0, ).reshape((big_triangles.shape[0], 1)) - def normalize_input(arr, arr_name=""): + @warn_on_args_to_kwargs() + def normalize_input(arr, *, arr_name=""): if ( isinstance(arr, (tuple, list, np.ndarray)) and len(arr) in [3, 4] @@ -192,12 +196,12 @@ def normalize_input(arr, arr_name=""): return np.array(arr) # update colors - colors = normalize_input(colors, "colors") + colors = normalize_input(colors, arr_name="colors") big_colors = np.repeat(colors, unit_verts_size, axis=0) big_colors *= 255 # update orientations - directions = normalize_input(directions, "directions") + directions = normalize_input(directions, arr_name="directions") for pts, dirs in enumerate(directions): # Normal vector of the object. dir_abs = np.linalg.norm(dirs) @@ -295,7 +299,8 @@ def prim_box(): return vertices, triangles -def prim_sphere(name="symmetric362", gen_faces=False, phi=None, theta=None): +@warn_on_args_to_kwargs() +def prim_sphere(*, name="symmetric362", gen_faces=False, phi=None, theta=None): """Provide vertices and triangles of the spheres. Parameters @@ -413,7 +418,7 @@ def _fexp(x, p): """Return a different kind of exponentiation.""" return np.sign(x) * (np.abs(x) ** p) - sphere_verts, sphere_triangles = prim_sphere(sphere_name) + sphere_verts, sphere_triangles = prim_sphere(name=sphere_name) _, sphere_phi, sphere_theta = cart2sphere(*sphere_verts.T) phi, theta = roundness @@ -605,7 +610,8 @@ def prim_rhombicuboctahedron(): return vertices, triangles -def prim_star(dim=2): +@warn_on_args_to_kwargs() +def prim_star(*, dim=2): """Return vertices and triangle for star geometry. Parameters @@ -918,7 +924,8 @@ def prim_frustum(): return vertices, triangles -def prim_cylinder(radius=0.5, height=1, sectors=36, capped=True): +@warn_on_args_to_kwargs() +def prim_cylinder(*, radius=0.5, height=1, sectors=36, capped=True): """Return vertices and triangles for a cylinder. Parameters @@ -1045,8 +1052,14 @@ def prim_cylinder(radius=0.5, height=1, sectors=36, capped=True): return vertices, triangles +@warn_on_args_to_kwargs() def prim_arrow( - height=1.0, resolution=10, tip_length=0.35, tip_radius=0.1, shaft_radius=0.03 + *, + height=1.0, + resolution=10, + tip_length=0.35, + tip_radius=0.1, + shaft_radius=0.03, ): """Return vertices and triangle for arrow geometry. @@ -1135,7 +1148,8 @@ def prim_arrow( return vertices, triangles -def prim_cone(radius=0.5, height=1, sectors=10): +@warn_on_args_to_kwargs() +def prim_cone(*, radius=0.5, height=1, sectors=10): """Return vertices and triangle of a Cone. Parameters diff --git a/fury/shaders/tests/test_base.py b/fury/shaders/tests/test_base.py index 7fc7f3438..14c518c59 100644 --- a/fury/shaders/tests/test_base.py +++ b/fury/shaders/tests/test_base.py @@ -191,7 +191,7 @@ def test_add_shader_callback(): scene = window.Scene() scene.add(cube) - showm = window.ShowManager(scene) + showm = window.ShowManager(scene=scene) class Timer: idx = 0.0 diff --git a/fury/testing.py b/fury/testing.py index 79f790f7a..7904f77c4 100644 --- a/fury/testing.py +++ b/fury/testing.py @@ -13,6 +13,8 @@ from numpy.testing import assert_array_equal import scipy # type: ignore +from fury.decorators import warn_on_args_to_kwargs + @contextmanager def captured_output(): @@ -37,13 +39,18 @@ def captured_output(): sys.stdout, sys.stderr = old_out, old_err -def assert_operator(value1, value2, msg="", op=operator.eq): +@warn_on_args_to_kwargs() +def assert_operator(value1, value2, *, msg="", op=operator.eq): """Check Boolean statement.""" if not op(value1, value2): raise AssertionError(msg.format(str(value2), str(value1))) -assert_greater_equal = partial(assert_operator, op=operator.ge, msg="{0} >= {1}") +assert_greater_equal = partial( + assert_operator, + op=operator.ge, + msg="{0} >= {1}", +) assert_greater = partial(assert_operator, op=operator.gt, msg="{0} > {1}") assert_less_equal = partial(assert_operator, op=operator.le, msg="{0} =< {1}") assert_less = partial(assert_operator, op=operator.lt, msg="{0} < {1}") @@ -63,7 +70,8 @@ def assert_arrays_equal(arrays1, arrays2): class EventCounter: - def __init__(self, events_names=None): + @warn_on_args_to_kwargs() + def __init__(self, *, events_names=None): if events_names is None: events_names = [ "CharEvent", @@ -113,7 +121,11 @@ def check_counts(self, expected): msg = "Wrong count for '{}'." for event, count in expected.events_counts.items(): - assert_equal(self.events_counts[event], count, msg=msg.format(event)) + assert_equal( + self.events_counts[event], + count, + msg=msg.format(event), + ) class clear_and_catch_warnings(warnings.catch_warnings): @@ -161,7 +173,8 @@ class clear_and_catch_warnings(warnings.catch_warnings): class_modules = () - def __init__(self, record=True, modules=()): + @warn_on_args_to_kwargs() + def __init__(self, *, record=True, modules=()): self.modules = set(modules).union(self.class_modules) self._warnreg_copies = {} super(clear_and_catch_warnings, self).__init__(record=record) diff --git a/fury/tests/test_actors.py b/fury/tests/test_actors.py index 46625250d..b76fec299 100644 --- a/fury/tests/test_actors.py +++ b/fury/tests/test_actors.py @@ -48,8 +48,8 @@ def test_slicer(verbose=False): scene = window.Scene() data = 255 * np.random.rand(50, 50, 50) affine = np.eye(4) - slicer = actor.slicer(data, affine, value_range=[data.min(), data.max()]) - slicer.display(None, None, 25) + slicer = actor.slicer(data, affine=affine, value_range=[data.min(), data.max()]) + slicer.display(x=None, y=None, z=25) scene.add(slicer) scene.reset_camera() @@ -57,7 +57,7 @@ def test_slicer(verbose=False): # window.show(scene) # copy pixels in numpy array directly - arr = window.snapshot(scene, "test_slicer.png", offscreen=True) + arr = window.snapshot(scene, fname="test_slicer.png", offscreen=True) if verbose: print(arr.sum()) @@ -80,14 +80,22 @@ def test_slicer(verbose=False): # save pixels in png file not a numpy array with InTemporaryDirectory() as tmpdir: fname = os.path.join(tmpdir, "slice.png") - window.snapshot(scene, fname, offscreen=True) + window.snapshot(scene, fname=fname, offscreen=True) report = window.analyze_snapshot(fname, find_objects=True) npt.assert_equal(report.objects, 1) # Test Errors data_4d = 255 * np.random.rand(50, 50, 50, 50) - npt.assert_raises(ValueError, actor.slicer, data_4d) - npt.assert_raises(ValueError, actor.slicer, np.ones(10)) + npt.assert_raises( + ValueError, + actor.slicer, + data_4d, + ) + npt.assert_raises( + ValueError, + actor.slicer, + np.ones(10), + ) scene.clear() @@ -114,9 +122,9 @@ def test_slicer(verbose=False): scene.clear() slicer_lut = actor.slicer(data, lookup_colormap=lut) - slicer_lut.display(10, None, None) - slicer_lut.display(None, 10, None) - slicer_lut.display(None, None, 10) + slicer_lut.display(x=10, y=None, z=None) + slicer_lut.display(x=None, y=10, z=None) + slicer_lut.display(x=None, y=None, z=10) slicer_lut.opacity(0.5) slicer_lut.tolerance(0.03) @@ -125,7 +133,7 @@ def test_slicer(verbose=False): npt.assert_equal(slicer_lut2.picker.GetTolerance(), 0.03) slicer_lut2.opacity(1) slicer_lut2.tolerance(0.025) - slicer_lut2.display(None, None, 10) + slicer_lut2.display(x=None, y=None, z=10) scene.add(slicer_lut2) scene.reset_clipping_range() @@ -138,8 +146,8 @@ def test_slicer(verbose=False): data = 255 * np.random.rand(50, 50, 50) affine = np.diag([1, 3, 2, 1]) - slicer = actor.slicer(data, affine, interpolation="nearest") - slicer.display(None, None, 25) + slicer = actor.slicer(data, affine=affine, interpolation="nearest") + slicer.display(x=None, y=None, z=25) scene.add(slicer) scene.reset_camera() @@ -187,7 +195,7 @@ def test_surface(): ) scene.add(surface_actor) # window.show(scene, size=(600, 600), reset_camera=False) - arr = window.snapshot(scene, "test_surface.png", offscreen=True) + arr = window.snapshot(scene, fname="test_surface.png", offscreen=True) report = window.analyze_snapshot(arr, find_objects=True) npt.assert_equal(report.objects, 1) @@ -200,7 +208,7 @@ def test_contour_from_roi(interactive=False): data[25, 20:30, 25] = 1.0 affine = np.eye(4) surface = actor.contour_from_roi( - data, affine, color=np.array([1, 0, 1]), opacity=0.5 + data, affine=affine, color=np.array([1, 0, 1]), opacity=0.5 ) scene.add(surface) @@ -219,7 +227,7 @@ def test_contour_from_roi(interactive=False): data2[35:40, 25, 25] = 1.0 affine = np.eye(4) surface2 = actor.contour_from_roi( - data2, affine, color=np.array([0, 1, 1]), opacity=0.5 + data2, affine=affine, color=np.array([0, 1, 1]), opacity=0.5 ) scene2.add(surface2) @@ -228,8 +236,8 @@ def test_contour_from_roi(interactive=False): if interactive: window.show(scene2) - arr = window.snapshot(scene, "test_surface.png", offscreen=True) - arr2 = window.snapshot(scene2, "test_surface2.png", offscreen=True) + arr = window.snapshot(scene, fname="test_surface.png", offscreen=True) + arr2 = window.snapshot(scene2, fname="test_surface2.png", offscreen=True) report = window.analyze_snapshot(arr, find_objects=True) report2 = window.analyze_snapshot(arr2, find_objects=True) @@ -287,10 +295,10 @@ def test_contour_from_label(interactive=False): window.show(scene2) arr = window.snapshot( - scene, "test_surface.png", offscreen=True, order_transparent=False + scene, fname="test_surface.png", offscreen=True, order_transparent=False ) arr2 = window.snapshot( - scene2, "test_surface2.png", offscreen=True, order_transparent=True + scene2, fname="test_surface2.png", offscreen=True, order_transparent=True ) report = window.analyze_snapshot( @@ -312,14 +320,14 @@ def test_streamtube_and_line_actors(): lines = [line1, line2] colors = np.array([[1, 0, 0], [0, 0, 1.0]]) - c = actor.line(lines, colors, linewidth=3) + c = actor.line(lines, colors=colors, linewidth=3) scene.add(c) - c = actor.line(lines, colors, spline_subdiv=5, linewidth=3) + c = actor.line(lines, colors=colors, spline_subdiv=5, linewidth=3) scene.add(c) # create streamtubes of the same lines and shift them a bit - c2 = actor.streamtube(lines, colors, linewidth=0.1) + c2 = actor.streamtube(lines, colors=colors, linewidth=0.1) c2.SetPosition(2, 0, 0) scene.add(c2) @@ -333,7 +341,7 @@ def test_streamtube_and_line_actors(): npt.assert_equal(report.colors_found, [True, True]) # as before with splines - c2 = actor.streamtube(lines, colors, spline_subdiv=5, linewidth=0.1) + c2 = actor.streamtube(lines, colors=colors, spline_subdiv=5, linewidth=0.1) c2.SetPosition(2, 0, 0) scene.add(c2) @@ -346,7 +354,7 @@ def test_streamtube_and_line_actors(): npt.assert_equal(report.objects, 4) npt.assert_equal(report.colors_found, [True, True]) - c3 = actor.line(lines, colors, depth_cue=True, fake_tube=True) + c3 = actor.line(lines, colors=colors, depth_cue=True, fake_tube=True) shader_obj = c3.GetShaderProperty() mapper_code = shader_obj.GetGeometryShaderCode() @@ -355,9 +363,9 @@ def test_streamtube_and_line_actors(): npt.assert_equal(c3.GetProperty().GetRenderLinesAsTubes(), True) - c4 = actor.streamtube(lines, colors, replace_strips=False) + c4 = actor.streamtube(lines, colors=colors, replace_strips=False) - c5 = actor.streamtube(lines, colors, replace_strips=True) + c5 = actor.streamtube(lines, colors=colors, replace_strips=True) strips4 = c4.GetMapper().GetInput().GetStrips().GetData().GetSize() strips5 = c5.GetMapper().GetInput().GetStrips().GetData().GetSize() @@ -398,9 +406,9 @@ def test_bundle_maps(): value_range=(1.0, 1), ) - line = actor.line(bundle, metric, linewidth=0.1, lookup_colormap=lut) + line = actor.line(bundle, colors=metric, linewidth=0.1, lookup_colormap=lut) scene.add(line) - scene.add(actor.scalar_bar(lut, " ")) + scene.add(actor.scalar_bar(lookup_table=lut, title=" ")) report = window.analyze_scene(scene) @@ -413,7 +421,7 @@ def test_bundle_maps(): values = 100 * np.random.rand(nb_points) # values[:nb_points/2] = 0 - line = actor.streamtube(bundle, values, linewidth=0.1, lookup_colormap=lut) + line = actor.streamtube(bundle, colors=values, linewidth=0.1, lookup_colormap=lut) scene.add(line) # window.show(scene) @@ -425,7 +433,7 @@ def test_bundle_maps(): colors = np.random.rand(nb_points, 3) # values[:nb_points/2] = 0 - line = actor.line(bundle, colors, linewidth=2) + line = actor.line(bundle, colors=colors, linewidth=2) scene.add(line) # window.show(scene) @@ -439,8 +447,8 @@ def test_bundle_maps(): # try other input options for colors scene.clear() - actor.line(bundle, (1.0, 0.5, 0)) - actor.line(bundle, np.arange(len(bundle))) + actor.line(bundle, colors=(1.0, 0.5, 0)) + actor.line(bundle, colors=np.arange(len(bundle))) actor.line(bundle) colors = [np.random.rand(*b.shape) for b in bundle] actor.line(bundle, colors=colors) @@ -452,7 +460,7 @@ def test_odf_slicer(interactive=False): # vertices and faces of a sphere rather that needing # a specific type of sphere. We can use prim_sphere # as an alternative to get_sphere. - vertices, faces = prim_sphere("repulsion100", True) + vertices, faces = prim_sphere(name="repulsion100", gen_faces=True) sphere = Sphere() sphere.vertices = vertices sphere.faces = faces @@ -521,13 +529,13 @@ def test_odf_slicer(interactive=False): # Test that odf_slicer.display works properly scene.clear() scene.add(odf_actor) - scene.add(actor.axes((11, 11, 11))) + scene.add(actor.axes(scale=(11, 11, 11))) for i in range(11): - odf_actor.display(i, None, None) + odf_actor.display(x=i, y=None, z=None) if interactive: window.show(scene) for j in range(11): - odf_actor.display(None, j, None) + odf_actor.display(x=None, y=j, z=None) if interactive: window.show(scene) @@ -562,7 +570,7 @@ def test_odf_slicer(interactive=False): global_cm=True, ) - vertices2, faces2 = prim_sphere("repulsion200", True) + vertices2, faces2 = prim_sphere(name="repulsion200", gen_faces=True) sphere2 = Sphere() sphere2.vertices = vertices2 sphere2.faces = faces2 @@ -607,13 +615,13 @@ def test_peak_slicer(interactive=False): scene = window.Scene() peak_actor = actor.peak_slicer(peak_dirs) scene.add(peak_actor) - scene.add(actor.axes((11, 11, 11))) + scene.add(actor.axes(scale=(11, 11, 11))) if interactive: window.show(scene) scene.clear() scene.add(peak_actor) - scene.add(actor.axes((11, 11, 11))) + scene.add(actor.axes(scale=(11, 11, 11))) for k in range(11): peak_actor.display_extent(0, 10, 0, 10, k, k) @@ -621,13 +629,13 @@ def test_peak_slicer(interactive=False): peak_actor.display_extent(0, 10, j, j, 0, 10) for i in range(11): - peak_actor.display(i, None, None) + peak_actor.display(x=i, y=None, z=None) scene.rm_all() peak_actor_sym = actor.peak_slicer( peak_dirs, - peak_values, + peaks_values=peak_values, mask=None, affine=np.diag([3, 2, 1, 1]), colors=None, @@ -640,7 +648,7 @@ def test_peak_slicer(interactive=False): peak_actor_asym = actor.peak_slicer( peak_dirs, - peak_values, + peaks_values=peak_values, mask=None, affine=np.diag([3, 2, 1, 1]), colors=None, @@ -654,7 +662,7 @@ def test_peak_slicer(interactive=False): scene.add(peak_actor_sym) scene.add(peak_actor_asym) - scene.add(actor.axes((11, 11, 11))) + scene.add(actor.axes(scale=(11, 11, 11))) if interactive: window.show(scene) @@ -739,7 +747,7 @@ def test_tensor_slicer(interactive=False): mevals[..., :] = evals mevecs[..., :, :] = evecs - vertices, faces = prim_sphere("symmetric724", True) + vertices, faces = prim_sphere(name="symmetric724", gen_faces=True) sphere = Sphere() sphere.vertices = vertices sphere.faces = faces @@ -892,7 +900,7 @@ def test_points(interactive=False): def test_vector_text(interactive=False): npt.assert_raises(ExpiredDeprecationError, actor.label, "FURY Rocks") - text_actor = actor.vector_text("FURY Rocks", direction=None) + text_actor = actor.vector_text(text="FURY Rocks", direction=None) scene = window.Scene() scene.add(text_actor) @@ -904,35 +912,35 @@ def test_vector_text(interactive=False): if interactive: window.show(scene, reset_camera=False) - text_actor = actor.vector_text("FURY Rocks") + text_actor = actor.vector_text(text="FURY Rocks") npt.assert_equal(scene.GetActors().GetNumberOfItems(), 1) center = np.array(text_actor.GetCenter()) [assert_greater_equal(v, 0) for v in center] - text_actor_centered = actor.vector_text("FURY Rocks", align_center=True) + text_actor_centered = actor.vector_text(text="FURY Rocks", align_center=True) center = np.array(text_actor_centered.GetCenter()) npt.assert_equal(center, np.zeros(3)) - text_actor_rot_1 = actor.vector_text("FURY Rocks", direction=(1, 1, 1)) - text_actor_rot_2 = actor.vector_text("FURY Rocks", direction=(1, 1, 0)) + text_actor_rot_1 = actor.vector_text(text="FURY Rocks", direction=(1, 1, 1)) + text_actor_rot_2 = actor.vector_text(text="FURY Rocks", direction=(1, 1, 0)) center_1 = text_actor_rot_1.GetCenter() center_2 = text_actor_rot_2.GetCenter() assert_not_equal(np.linalg.norm(center_1), np.linalg.norm(center_2)) # test centered - text_centered = actor.vector_text("FURY Rocks", align_center=True) + text_centered = actor.vector_text(text="FURY Rocks", align_center=True) center_3 = text_centered.GetCenter() npt.assert_almost_equal(np.linalg.norm(center_3), 0.0) text_extruded = actor.vector_text( - "FURY Rocks", scale=(0.2, 0.2, 0.2), extrusion=1.123 + text="FURY Rocks", scale=(0.2, 0.2, 0.2), extrusion=1.123 ) z_max = text_extruded.GetBounds()[-1] npt.assert_almost_equal(z_max, 1.123) text_extruded_centered = actor.vector_text( - "FURY Rocks", + text="FURY Rocks", scale=(0.2, 0.2, 0.2), direction=None, align_center=True, @@ -1253,16 +1261,31 @@ def test_container(): def test_grid(_interactive=False): vol1 = np.zeros((100, 100, 100)) vol1[25:75, 25:75, 25:75] = 100 - contour_actor1 = actor.contour_from_roi(vol1, np.eye(4), (1.0, 0, 0), 1.0) + contour_actor1 = actor.contour_from_roi( + vol1, + affine=np.eye(4), + color=(1.0, 0, 0), + opacity=1.0, + ) vol2 = np.zeros((100, 100, 100)) vol2[25:75, 25:75, 25:75] = 100 - contour_actor2 = actor.contour_from_roi(vol2, np.eye(4), (1.0, 0.5, 0), 1.0) + contour_actor2 = actor.contour_from_roi( + vol2, + affine=np.eye(4), + color=(1.0, 0.5, 0), + opacity=1.0, + ) vol3 = np.zeros((100, 100, 100)) vol3[25:75, 25:75, 25:75] = 100 - contour_actor3 = actor.contour_from_roi(vol3, np.eye(4), (1.0, 0.5, 0.5), 1.0) + contour_actor3 = actor.contour_from_roi( + vol3, + affine=np.eye(4), + color=(1.0, 0.5, 0.5), + opacity=1.0, + ) scene = window.Scene() actors = [] @@ -1303,11 +1326,11 @@ def test_grid(_interactive=False): scene.add(container) - scene.projection("orthogonal") + scene.projection(proj_type="orthogonal") counter = itertools.count() - show_m = window.ShowManager(scene) + show_m = window.ShowManager(scene=scene) def timer_callback(_obj, _event): nonlocal counter @@ -1329,7 +1352,7 @@ def timer_callback(_obj, _event): scene.rm_all() counter = itertools.count() - show_m = window.ShowManager(scene) + show_m = window.ShowManager(scene=scene) # show the grid with the captions container = grid( @@ -1461,8 +1484,8 @@ def test_matplotlib_figure(): arr = matplotlib_figure_to_numpy(fig, dpi=500, transparent=True) plt.close("all") - fig_actor = actor.figure(arr, "cubic") - fig_actor2 = actor.figure(arr, "cubic") + fig_actor = actor.figure(arr, interpolation="cubic") + fig_actor2 = actor.figure(arr, interpolation="cubic") scene = window.Scene() scene.background((1, 1, 1.0)) @@ -1473,7 +1496,7 @@ def test_matplotlib_figure(): ax_actor.SetPosition(-50, 500, -800) fig_actor2.SetPosition(500, 800, -400) display = window.snapshot( - scene, "test_mpl.png", order_transparent=False, offscreen=True + scene, fname="test_mpl.png", order_transparent=False, offscreen=True ) _ = window.analyze_snapshot( display, bg_color=(255, 255, 255.0), colors=[(31, 119, 180)], find_objects=False @@ -1643,7 +1666,13 @@ def test_sdf_actor(interactive=False): scales = [1, 2, 3, 4] primitive = ["sphere", "ellipsoid", "torus", "capsule"] - sdf_actor = actor.sdf(centers, directions, colors, primitive, scales) + sdf_actor = actor.sdf( + centers, + directions=directions, + colors=colors, + primitives=primitive, + scales=scales, + ) scene.add(sdf_actor) scene.add(actor.axes()) if interactive: @@ -1656,7 +1685,13 @@ def test_sdf_actor(interactive=False): # Draw 3 spheres as the primitive type is str scene.clear() primitive = "sphere" - sdf_actor = actor.sdf(centers, directions, colors, primitive, scales) + sdf_actor = actor.sdf( + centers, + directions=directions, + colors=colors, + primitives=primitive, + scales=scales, + ) scene.add(sdf_actor) scene.add(actor.axes()) if interactive: @@ -1671,7 +1706,13 @@ def test_sdf_actor(interactive=False): scene.clear() primitive = ["sphere"] with npt.assert_warns(UserWarning): - sdf_actor = actor.sdf(centers, directions, colors, primitive, scales) + sdf_actor = actor.sdf( + centers, + directions=directions, + colors=colors, + primitives=primitive, + scales=scales, + ) scene.add(sdf_actor) scene.add(actor.axes()) @@ -1687,7 +1728,13 @@ def test_sdf_actor(interactive=False): scene.clear() primitive = ["sphere", "ellipsoid"] with npt.assert_warns(UserWarning): - sdf_actor = actor.sdf(centers, directions, colors, primitive, scales) + sdf_actor = actor.sdf( + centers, + directions=directions, + colors=colors, + primitives=primitive, + scales=scales, + ) scene.add(sdf_actor) scene.add(actor.axes()) diff --git a/fury/tests/test_colormap.py b/fury/tests/test_colormap.py index 14b79a117..653fe271e 100644 --- a/fury/tests/test_colormap.py +++ b/fury/tests/test_colormap.py @@ -71,8 +71,20 @@ def test_line_colors(): def test_create_colormap(): value = np.arange(25) - npt.assert_raises(ValueError, colormap.create_colormap, value.reshape((5, 5))) - npt.assert_raises(AttributeError, colormap.create_colormap, value, name="fake") + npt.assert_raises( + ValueError, + colormap.create_colormap, + value.reshape((5, 5)), + name="plasma", + auto=True, + ) + npt.assert_raises( + AttributeError, + colormap.create_colormap, + value, + name="fake", + auto=True, + ) npt.assert_warns( PendingDeprecationWarning, colormap.create_colormap, @@ -163,14 +175,16 @@ def test_color_converters(): illuminant = "D65" observer = "2" expected_lab = np.array([31.57976662, -1.86550104, -17.84845331]) - lab_color = colormap.rgb2lab(color, illuminant, observer) - rgb_color = colormap.lab2rgb(expected_lab, illuminant, observer) + lab_color = colormap.rgb2lab(color, illuminant=illuminant, observer=observer) + rgb_color = colormap.lab2rgb(expected_lab, illuminant=illuminant, observer=observer) npt.assert_almost_equal(lab_color, expected_lab) npt.assert_almost_equal(rgb_color, color) for color in colors: - lab_color = colormap.rgb2lab(color, illuminant, observer) - rgb_from_lab_color = colormap.lab2rgb(lab_color, illuminant, observer) + lab_color = colormap.rgb2lab(color, illuminant=illuminant, observer=observer) + rgb_from_lab_color = colormap.lab2rgb( + lab_color, illuminant=illuminant, observer=observer + ) npt.assert_almost_equal(rgb_from_lab_color, color) # testing rgb2hsv and hsv2rgb diff --git a/fury/tests/test_decorators.py b/fury/tests/test_decorators.py index 914424484..7e62b1fff 100644 --- a/fury/tests/test_decorators.py +++ b/fury/tests/test_decorators.py @@ -2,11 +2,13 @@ import numpy.testing as npt -from fury.decorators import doctest_skip_parser +import fury +from fury.decorators import doctest_skip_parser, warn_on_args_to_kwargs from fury.testing import assert_true HAVE_AMODULE = False HAVE_BMODULE = True +FURY_CURRENT_VERSION = fury.__version__ def test_skipper(): @@ -49,3 +51,45 @@ def f(): del HAVE_AMODULE f.__doc__ = docstring npt.assert_raises(NameError, doctest_skip_parser, f) + + +def test_warn_on_args_to_kwargs(): + @warn_on_args_to_kwargs() + def func(a, b, *, c, d=4, e=5): + return a + b + c + d + e + + # if FURY_CURRENT_VERSION is less than from_version + fury.__version__ = "0.0.0" + npt.assert_equal(func(1, 2, 3, 4, 5), 15) + npt.assert_equal(func(1, 2, c=3, d=4, e=5), 15) + npt.assert_raises(TypeError, func, 1, 3) + + # if FURY_CURRENT_VERSION is greater than until_version + fury.__version__ = "0.12.0" + npt.assert_equal(func(1, 2, c=3, d=4, e=5), 15) + npt.assert_equal(func(1, 2, c=3, d=5), 16) + npt.assert_equal(func(1, 2, c=3), 15) + npt.assert_raises(TypeError, func, 1, 3, 4) + npt.assert_raises(TypeError, func, 1, 3) + + # if FURY_CURRENT_VERSION is less than from_version + fury.__version__ = "0.10.0" + npt.assert_equal(func(1, 2, c=3, d=4, e=5), 15) + npt.assert_equal(func(1, 2, c=3, d=5), 16) + with npt.assert_warns(UserWarning): + npt.assert_equal(func(1, 2, 3, 4, 5), 15) + with npt.assert_warns(UserWarning): + npt.assert_equal(func(1, 2, 4), 16) + npt.assert_raises(TypeError, func, 1, 3) + + # if FURY_CURRENT_VERSION is equal to from_version + fury.__version__ = "0.11.0" + with npt.assert_warns(UserWarning): + npt.assert_equal(func(1, 2, 3, 6), 17) + with npt.assert_warns(UserWarning): + npt.assert_equal(func(1, 2, 10), 22) + npt.assert_raises(TypeError, func, 1, 2, e=10) + npt.assert_raises(TypeError, func, 1, 2, d=10) + npt.assert_raises(TypeError, func, 1, 3) + + fury.__version__ = FURY_CURRENT_VERSION diff --git a/fury/tests/test_deprecator.py b/fury/tests/test_deprecator.py index ca1bacad8..88f261eab 100644 --- a/fury/tests/test_deprecator.py +++ b/fury/tests/test_deprecator.py @@ -108,20 +108,20 @@ class CustomError(Exception): dec = deprecate_with_version func = dec("foo")(func_no_doc) - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(), None) npt.assert_equal(len(w), 1) assert_true(w[0].category is DeprecationWarning) npt.assert_equal(func.__doc__, "foo\n") func = dec("foo")(func_doc) - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(1), None) npt.assert_equal(len(w), 1) npt.assert_equal(func.__doc__, "A docstring\n\nfoo\n") func = dec("foo")(func_doc_long) - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(1, 2), None) npt.assert_equal(len(w), 1) @@ -130,12 +130,12 @@ class CustomError(Exception): # Try some since and until versions func = dec("foo", "0.2")(func_no_doc) npt.assert_equal(func.__doc__, "foo\n\n* deprecated from version: 0.2\n") - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(), None) npt.assert_equal(len(w), 1) func = dec("foo", until="10.6")(func_no_doc) - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(), None) npt.assert_equal(len(w), 1) @@ -168,14 +168,14 @@ class CustomError(Exception): # Check different warnings and errors func = dec("foo", warn_class=UserWarning)(func_no_doc) - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(), None) npt.assert_equal(len(w), 1) assert_true(w[0].category is UserWarning) func = dec("foo", error_class=CustomError)(func_no_doc) - with clear_and_catch_warnings(modules=[my_mod]) as w: + with clear_and_catch_warnings(record=True, modules=[my_mod]) as w: warnings.simplefilter("always") npt.assert_equal(func(), None) npt.assert_equal(len(w), 1) diff --git a/fury/tests/test_gltf.py b/fury/tests/test_gltf.py index 8e9febe2f..14e22ad9c 100644 --- a/fury/tests/test_gltf.py +++ b/fury/tests/test_gltf.py @@ -129,14 +129,14 @@ def test_export_gltf(): cube = actor.cube(np.add(centers, np.array([2, 0, 0])), colors=colors) scene.add(cube) - export_scene(scene, "test.gltf") + export_scene(scene, filename="test.gltf") gltf_obj = glTF("test.gltf") actors = gltf_obj.actors() npt.assert_equal(len(actors), 1) sphere = actor.sphere(centers, np.array([1, 0, 0]), use_primitive=False) scene.add(sphere) - export_scene(scene, "test.gltf") + export_scene(scene, filename="test.gltf") gltf_obj = glTF("test.gltf") actors = gltf_obj.actors() @@ -149,7 +149,7 @@ def test_export_gltf(): focal_point=(0.0, 0.0, 0.0), view_up=(0.0, 0.0, 1.0), ) - export_scene(scene, "test.gltf") + export_scene(scene, filename="test.gltf") gltf_obj = glTF("test.gltf") actors = gltf_obj.actors() @@ -167,7 +167,7 @@ def test_export_gltf(): gltf_obj = glTF(filename) box_actor = gltf_obj.actors() scene.add(*box_actor) - export_scene(scene, "test.gltf") + export_scene(scene, filename="test.gltf") scene.clear() gltf_obj = glTF("test.gltf") @@ -192,7 +192,7 @@ def test_simple_animation(): animation = gltf_obj.main_animation() timeline.add_animation(animation) scene = window.Scene() - showm = window.ShowManager(scene, size=(900, 768)) + showm = window.ShowManager(scene=scene, size=(900, 768)) showm.initialize() scene.add(timeline) @@ -259,7 +259,7 @@ def test_skinning(): npt.assert_equal(len(ibms), 2) scene = window.Scene() - showm = window.ShowManager(scene, size=(900, 768)) + showm = window.ShowManager(scene=scene, size=(900, 768)) showm.initialize() scene.add(timeline) @@ -333,7 +333,7 @@ def test_morphing(): gltf_obj.update_morph(anim_1) scene = window.Scene() - showm = window.ShowManager(scene, size=(900, 768)) + showm = window.ShowManager(scene=scene, size=(900, 768)) showm.initialize() timeline_1 = Timeline() diff --git a/fury/tests/test_interactor.py b/fury/tests/test_interactor.py index 33c6132ce..5608bde62 100644 --- a/fury/tests/test_interactor.py +++ b/fury/tests/test_interactor.py @@ -24,7 +24,10 @@ def test_custom_interactor_style_events(recording=False): # in steps so that the widgets can be added properly interactor_style = interactor.CustomInteractorStyle() show_manager = window.ShowManager( - scene, size=(800, 800), reset_camera=False, interactor_style=interactor_style + scene=scene, + size=(800, 800), + reset_camera=False, + interactor_style=interactor_style, ) # Create a cursor, a circle that will follow the mouse. @@ -56,8 +59,8 @@ def follow_mouse(iren, obj): np.array([[-1, 1, 0.0], [1, 1, 0.0]]), ] colors = np.array([[1.0, 0.0, 0.0], [0.3, 0.7, 0.0]]) - tube1 = actor.streamtube([lines[0]], colors[0]) - tube2 = actor.streamtube([lines[1]], colors[1]) + tube1 = actor.streamtube([lines[0]], colors=colors[0]) + tube2 = actor.streamtube([lines[1]], colors=colors[1]) scene.add(tube1) scene.add(tube2) @@ -157,8 +160,8 @@ def test_double_click_events(recording=False): cube = actor.cube( np.array([(0, 0, 0)]), - np.array([(0.16526678, 0.0186237, 0.01906076)]), - (1, 1, 1), + directions=np.array([(0.16526678, 0.0186237, 0.01906076)]), + colors=(1, 1, 1), scales=3, ) diff --git a/fury/tests/test_io.py b/fury/tests/test_io.py index 277a25e2e..84d0c679e 100644 --- a/fury/tests/test_io.py +++ b/fury/tests/test_io.py @@ -44,8 +44,22 @@ def test_save_and_load_polydata(): npt.assert_array_equal(data, out_data) - npt.assert_raises(IOError, save_polydata, PolyData(), "test.vti") - npt.assert_raises(IOError, save_polydata, PolyData(), "test.obj") + npt.assert_raises( + IOError, + save_polydata, + PolyData(), + "test.vti", + binary=False, + color_array_name=None, + ) + npt.assert_raises( + IOError, + save_polydata, + PolyData(), + "test.obj", + binary=False, + color_array_name=None, + ) npt.assert_raises(IOError, load_polydata, "test.vti") npt.assert_raises(FileNotFoundError, load_polydata, "does-not-exist.obj") @@ -136,21 +150,56 @@ def test_save_load_image(): data[..., 0], out_image[..., 0], decimal=0 ) - npt.assert_raises(IOError, load_image, invalid_link) - npt.assert_raises(IOError, load_image, "test.vtk") - npt.assert_raises(IOError, load_image, "test.vtk", use_pillow=False) npt.assert_raises( - IOError, save_image, np.random.randint(0, 255, size=(50, 3)), "test.vtk" + IOError, + load_image, + invalid_link, + as_vtktype=False, + use_pillow=True, + ) + npt.assert_raises( + IOError, + load_image, + "test.vtk", + as_vtktype=False, + use_pillow=True, + ) + npt.assert_raises( + IOError, + load_image, + "test.vtk", + as_vtktype=False, + use_pillow=False, ) npt.assert_raises( IOError, save_image, np.random.randint(0, 255, size=(50, 3)), "test.vtk", + compression_quality=75, + compression_type="deflation", + use_pillow=True, + dpi=(72, 72), + ) + npt.assert_raises( + IOError, + save_image, + np.random.randint(0, 255, size=(50, 3)), + "test.vtk", + compression_quality=75, + compression_type="deflation", use_pillow=False, + dpi=(72, 72), ) npt.assert_raises( - IOError, save_image, np.random.randint(0, 255, size=(50, 3, 1, 1)), "test.png" + IOError, + save_image, + np.random.randint(0, 255, size=(50, 3, 1, 1)), + "test.png", + compression_quality=75, + compression_type="deflation", + use_pillow=True, + dpi=(72, 72), ) compression_type = [None, "bits", "random"] @@ -221,7 +270,13 @@ def test_load_cubemap_texture(): save_image(data, fname_path) fnames = [fname_path] * 5 - npt.assert_raises(IOError, load_cubemap_texture, fnames) + npt.assert_raises( + IOError, + load_cubemap_texture, + fnames, + interpolate_on=True, + mipmap_on=True, + ) fnames = [fname_path] * 6 texture = load_cubemap_texture(fnames) @@ -234,7 +289,13 @@ def test_load_cubemap_texture(): ) fnames = [fname_path] * 7 - npt.assert_raises(IOError, load_cubemap_texture, fnames) + npt.assert_raises( + IOError, + load_cubemap_texture, + fnames, + interpolate_on=True, + mipmap_on=True, + ) def test_load_sprite_sheet(): diff --git a/fury/tests/test_layout.py b/fury/tests/test_layout.py index e7480b46c..48f3b0ce3 100644 --- a/fury/tests/test_layout.py +++ b/fury/tests/test_layout.py @@ -46,11 +46,17 @@ def get_default_cubes( cube_first_scale, cube_second_scale = scales cube_first = actor.cube( - cube_first_center, cube_first_direction, cube_first_color, cube_first_scale + cube_first_center, + directions=cube_first_direction, + colors=cube_first_color, + scales=cube_first_scale, ) cube_second = actor.cube( - cube_second_center, cube_second_direction, cube_second_color, cube_second_scale + cube_second_center, + directions=cube_second_direction, + colors=cube_second_color, + scales=cube_second_scale, ) return (cube_first, cube_second) @@ -263,7 +269,11 @@ def test_x_layout(): negative_x_layout = XLayout(direction="x-") with npt.assert_raises(ValueError): - _ = XLayout(direction="Invalid direction") + _ = XLayout( + direction="Invalid direction", + cell_padding=0, + cell_shape="rect", + ) positive_positions = positive_x_layout.compute_positions(actors) negative_positions = negative_x_layout.compute_positions(actors) @@ -300,7 +310,11 @@ def test_y_layout(): negative_y_layout = YLayout(direction="y-") with npt.assert_raises(ValueError): - _ = YLayout(direction="Invalid direction") + _ = YLayout( + direction="Invalid direction", + cell_padding=0, + cell_shape="rect", + ) positive_positions = positive_y_layout.compute_positions(actors) negative_positions = negative_y_layout.compute_positions(actors) @@ -338,7 +352,11 @@ def test_z_layout(): diagonal_z_layout = ZLayout(direction="z+", cell_shape="diagonal") with npt.assert_raises(ValueError): - _ = XLayout(direction="Invalid direction") + _ = XLayout( + direction="Invalid direction", + cell_padding=0, + cell_shape="rect", + ) with npt.assert_raises(ValueError): invalid_shape_layout = ZLayout(direction="z+", cell_shape="Invalid Shape") diff --git a/fury/tests/test_molecular.py b/fury/tests/test_molecular.py index f3ab32116..2a1aa5099 100644 --- a/fury/tests/test_molecular.py +++ b/fury/tests/test_molecular.py @@ -10,12 +10,12 @@ def test_periodic_table(): npt.assert_equal(table.atomic_number("C"), 6) npt.assert_equal(table.element_name(7), "Nitrogen") npt.assert_equal(table.atomic_symbol(8), "O") - npt.assert_allclose(table.atomic_radius(1, "VDW"), 1.2, 0.1, 0) - npt.assert_allclose(table.atomic_radius(6, "Covalent"), 0.75, 0.1, 0) + npt.assert_allclose(table.atomic_radius(1, radius_type="VDW"), 1.2, 0.1, 0) + npt.assert_allclose(table.atomic_radius(6, radius_type="Covalent"), 0.75, 0.1, 0) npt.assert_array_almost_equal(table.atom_color(1), np.array([1, 1, 1])) # Test errors - npt.assert_raises(ValueError, table.atomic_radius, 4, "test") + npt.assert_raises(ValueError, table.atomic_radius, 4, radius_type="test") def get_default_molecular_info(all_info=False): @@ -64,17 +64,41 @@ def test_molecule_creation(): # Test errors elements = np.array([6, 6]) - npt.assert_raises(ValueError, mol.Molecule, elements, atom_coords) + npt.assert_raises( + ValueError, + mol.Molecule, + atomic_numbers=elements, + coords=atom_coords, + atom_names=None, + model=None, + residue_seq=None, + chain=None, + sheet=None, + helix=None, + is_hetatm=None, + ) elements = list(range(8)) - npt.assert_raises(ValueError, mol.Molecule, elements, atom_coords) + npt.assert_raises( + ValueError, + mol.Molecule, + atomic_numbers=elements, + coords=atom_coords, + atom_names=None, + model=None, + residue_seq=None, + chain=None, + sheet=None, + helix=None, + is_hetatm=None, + ) def test_add_atom_bond_creation(): molecule = mol.Molecule() mol.add_atom(molecule, 6, 0, 0, 0) mol.add_atom(molecule, 6, 1, 0, 0) - mol.add_bond(molecule, 0, 1, 1) + mol.add_bond(molecule, 0, 1, bond_order=1) npt.assert_equal(molecule.total_num_bonds, 1) npt.assert_equal(molecule.total_num_atoms, 2) @@ -116,7 +140,7 @@ def test_bond_order(): molecule = mol.Molecule() mol.add_atom(molecule, 6, 0, 0, 0) mol.add_atom(molecule, 6, 1, 0, 0) - mol.add_bond(molecule, 0, 1, 3) + mol.add_bond(molecule, 0, 1, bond_order=3) npt.assert_equal(mol.get_bond_order(molecule, 0), 3) # Testing set_bond_order @@ -131,7 +155,7 @@ def test_deep_copy_molecule(): molecule1 = mol.Molecule() mol.add_atom(molecule1, 6, 0, 0, 0) mol.add_atom(molecule1, 6, 1, 0, 0) - mol.add_bond(molecule1, 0, 1, 1) + mol.add_bond(molecule1, 0, 1, bond_order=1) molecule2 = mol.Molecule() mol.deep_copy_molecule(molecule2, molecule1) npt.assert_equal(molecule2.total_num_bonds, 1) @@ -140,14 +164,14 @@ def test_deep_copy_molecule(): def test_compute_bonding(): atomic_numbers, atom_coords = get_default_molecular_info() - molecule = mol.Molecule(atomic_numbers, atom_coords) + molecule = mol.Molecule(atomic_numbers=atomic_numbers, coords=atom_coords) mol.compute_bonding(molecule) npt.assert_equal(molecule.total_num_bonds, 7) def test_sphere_cpk(interactive=False): atomic_numbers, atom_coords = get_default_molecular_info() - molecule = mol.Molecule(atomic_numbers, atom_coords) + molecule = mol.Molecule(atomic_numbers=atomic_numbers, coords=atom_coords) table = mol.PTable() colormodes = ["discrete", "single"] colors = np.array( @@ -159,7 +183,7 @@ def test_sphere_cpk(interactive=False): ) scene = window.Scene() for i, colormode in enumerate(colormodes): - test_actor = mol.sphere_cpk(molecule, colormode) + test_actor = mol.sphere_cpk(molecule, colormode=colormode) scene.add(test_actor) scene.reset_camera() @@ -176,7 +200,7 @@ def test_sphere_cpk(interactive=False): scene.clear() # Testing warnings - npt.assert_warns(UserWarning, mol.sphere_cpk, molecule, "multiple") + npt.assert_warns(UserWarning, mol.sphere_cpk, molecule, colormode="multiple") def test_bstick(interactive=False): @@ -185,9 +209,17 @@ def test_bstick(interactive=False): mol.add_atom(molecule, 6, 2, 0, 0) # Test errors for inadequate bonding data - npt.assert_raises(ValueError, mol.ball_stick, molecule) + npt.assert_raises( + ValueError, + mol.ball_stick, + molecule, + colormode="discrete", + atom_scale_factor=0.3, + bond_thickness=0.1, + multiple_bonds=True, + ) - mol.add_bond(molecule, 0, 1, 1) + mol.add_bond(molecule, 0, 1, bond_order=1) colormodes = ["discrete", "single"] atom_scale_factor = [0.3, 0.4] bond_thickness = [0.1, 0.2] @@ -204,10 +236,10 @@ def test_bstick(interactive=False): for i, colormode in enumerate(colormodes): test_actor = mol.ball_stick( molecule, - colormode, - atom_scale_factor[i], - bond_thickness[i], - multiple_bonds[i], + colormode=colormode, + atom_scale_factor=atom_scale_factor[i], + bond_thickness=bond_thickness[i], + multiple_bonds=multiple_bonds[i], ) scene.add(test_actor) scene.reset_camera() @@ -224,7 +256,15 @@ def test_bstick(interactive=False): scene.clear() # Testing warnings - npt.assert_warns(UserWarning, mol.ball_stick, molecule, "multiple") + npt.assert_warns( + UserWarning, + mol.ball_stick, + molecule, + colormode="multiple", + atom_scale_factor=0.3, + bond_thickness=0.1, + multiple_bonds=True, + ) def test_stick(interactive=False): @@ -233,8 +273,10 @@ def test_stick(interactive=False): mol.add_atom(molecule, 6, 2, 0, 0) # Test errors for inadequate bonding data - npt.assert_raises(ValueError, mol.stick, molecule) - mol.add_bond(molecule, 0, 1, 1) + npt.assert_raises( + ValueError, mol.stick, molecule, colormode="discrete", bond_thickness=0.1 + ) + mol.add_bond(molecule, 0, 1, bond_order=1) colormodes = ["discrete", "single"] bond_thickness = [0.1, 0.12] @@ -248,7 +290,11 @@ def test_stick(interactive=False): ) scene = window.Scene() for i, colormode in enumerate(colormodes): - test_actor = mol.stick(molecule, colormode, bond_thickness[i]) + test_actor = mol.stick( + molecule, + colormode=colormode, + bond_thickness=bond_thickness[i], + ) scene.add(test_actor) scene.reset_camera() scene.reset_clipping_range() @@ -264,7 +310,9 @@ def test_stick(interactive=False): scene.clear() # Testing warnings - npt.assert_warns(UserWarning, mol.stick, molecule, "multiple") + npt.assert_warns( + UserWarning, mol.stick, molecule, colormode="multiple", bond_thickness=0.1 + ) def test_ribbon(interactive=False): @@ -337,18 +385,22 @@ def test_ribbon(interactive=False): helix = secondary_structure sheet = [] molecule = mol.Molecule( - elements, - atom_coords, - atom_names, - model, - residue_seq, - chain, - sheet, - helix, - is_hetatm, + atomic_numbers=elements, + coords=atom_coords, + atom_names=atom_names, + model=model, + residue_seq=residue_seq, + chain=chain, + sheet=sheet, + helix=helix, + is_hetatm=is_hetatm, ) test_actor = mol.ribbon(molecule) - scene.set_camera((28, 113, 74), (34, 106, 70), (-0.37, 0.29, -0.88)) + scene.set_camera( + position=(28, 113, 74), + focal_point=(34, 106, 70), + view_up=(-0.37, 0.29, -0.88), + ) scene.add(test_actor) scene.reset_camera() scene.reset_clipping_range() @@ -367,7 +419,7 @@ def test_bounding_box(interactive=False): molecule = mol.Molecule() mol.add_atom(molecule, 6, 0, 0, 0) mol.add_atom(molecule, 6, 1, 1, 1) - mol.add_bond(molecule, 0, 1, 1) + mol.add_bond(molecule, 0, 1, bond_order=1) molecule_actor = mol.stick(molecule) test_box = mol.bounding_box(molecule, colors=(0, 1, 1), linewidth=0.1) diff --git a/fury/tests/test_primitive.py b/fury/tests/test_primitive.py index baac44a2b..9325eaa34 100644 --- a/fury/tests/test_primitive.py +++ b/fury/tests/test_primitive.py @@ -24,7 +24,7 @@ def test_vertices_primitives(): npt.assert_equal(vertices.min(), e_min) npt.assert_equal(vertices.max(), e_max) - vertices, _ = fp.prim_star(3) + vertices, _ = fp.prim_star(dim=3) npt.assert_equal(vertices.shape, (12, 3)) npt.assert_almost_equal(abs(np.mean(vertices)), 0.11111111) npt.assert_equal(vertices.min(), -3) @@ -108,7 +108,7 @@ def test_spheres_primitives(): ] for name, nb_verts, nb_triangles in l_primitives: - verts, faces = fp.prim_sphere(name) + verts, faces = fp.prim_sphere(name=name) npt.assert_equal(verts.shape, (nb_verts, 3)) npt.assert_almost_equal(np.mean(verts), 0) npt.assert_equal(len(faces), nb_triangles) @@ -116,7 +116,14 @@ def test_spheres_primitives(): list(set(np.concatenate(faces, axis=None))), list(range(len(verts))) ) - npt.assert_raises(ValueError, fp.prim_sphere, "sym362") + npt.assert_raises( + ValueError, + fp.prim_sphere, + name="sym362", + gen_faces=False, + phi=None, + theta=None, + ) l_primitives = [ (10, 10, 82, 160), @@ -138,7 +145,7 @@ def test_spheres_primitives(): def test_superquadric_primitives(): # test default, should be like a sphere 362 sq_verts, sq_faces = fp.prim_superquadric() - s_verts, s_faces = fp.prim_sphere("symmetric362") + s_verts, s_faces = fp.prim_sphere(name="symmetric362") npt.assert_equal(sq_verts.shape, s_verts.shape) npt.assert_equal(sq_faces.shape, s_faces.shape) @@ -211,7 +218,7 @@ def test_cone_primitive(): ) # test warnings - npt.assert_raises(ValueError, fp.prim_cone, 0.5, 1, 2) + npt.assert_raises(ValueError, fp.prim_cone, radius=0.5, height=1, sectors=2) def test_repeat_primitive(): @@ -223,9 +230,7 @@ def test_repeat_primitive(): for i in [-1, 1]: dirs = dirs * i - res = fp.repeat_primitive( - vertices=verts, faces=faces, centers=centers, directions=dirs, colors=colors - ) + res = fp.repeat_primitive(verts, faces, centers, directions=dirs, colors=colors) big_verts, big_faces, big_colors, big_centers = res @@ -263,8 +268,8 @@ def test_repeat_primitive_function(): phi_theta = np.array([[1, 1], [1, 2], [2, 1]]) _ = fp.repeat_primitive_function( - func=fp.prim_superquadric, - centers=centers, + fp.prim_superquadric, + centers, func_args=phi_theta, directions=dirs, colors=colors, diff --git a/fury/tests/test_stream.py b/fury/tests/test_stream.py index 5db6a05da..af2cf31b0 100644 --- a/fury/tests/test_stream.py +++ b/fury/tests/test_stream.py @@ -56,7 +56,7 @@ def test(use_raw_array, ms_stream=16): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -108,7 +108,7 @@ def test_pillow(): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -165,7 +165,7 @@ def test_rtc_video_stream_whitout_cython(loop: asyncio.AbstractEventLoop): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -212,7 +212,7 @@ def test(use_raw_array, ms_stream=16): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -275,7 +275,7 @@ def test(use_raw_array, ms_stream=16, whithout_iren_start=False): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -314,7 +314,7 @@ def test_stream_client_resize(): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -360,7 +360,7 @@ async def test(use_raw_array, ms_stream=16): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -448,7 +448,7 @@ def test(use_raw_array, ms_stream, whitouth_iren_start): scene = window.Scene() scene.add(actors) - showm = window.ShowManager(scene, size=(width_0, height_0)) + showm = window.ShowManager(scene=scene, size=(width_0, height_0)) stream = FuryStreamClient( showm, use_raw_array=use_raw_array, whithout_iren_start=whitouth_iren_start @@ -734,7 +734,7 @@ def test(use_raw_array): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, @@ -790,7 +790,7 @@ def test_widget(): scene = window.Scene() scene.add(actors) showm = window.ShowManager( - scene, + scene=scene, reset_camera=False, size=(width_0, height_0), order_transparent=False, diff --git a/fury/tests/test_testing.py b/fury/tests/test_testing.py index 39aae8bf7..416531353 100644 --- a/fury/tests/test_testing.py +++ b/fury/tests/test_testing.py @@ -1,5 +1,6 @@ """Testing file unittest.""" +import operator import sys import warnings @@ -48,7 +49,9 @@ def _add_to_scene(self, _scene): simple_ui = SimplestUI() current_size = (900, 600) scene = window.Scene() - show_manager = window.ShowManager(scene, size=current_size, title="FURY GridUI") + show_manager = window.ShowManager( + scene=scene, size=current_size, title="FURY GridUI" + ) scene.add(simple_ui) event_counter = ft.EventCounter() event_counter.monitor(simple_ui) @@ -69,14 +72,26 @@ def foo(): def test_assert(): - npt.assert_raises(AssertionError, ft.assert_false, True) - npt.assert_raises(AssertionError, ft.assert_true, False) - npt.assert_raises(AssertionError, ft.assert_less, 2, 1) - npt.assert_raises(AssertionError, ft.assert_less_equal, 2, 1) - npt.assert_raises(AssertionError, ft.assert_greater, 1, 2) - npt.assert_raises(AssertionError, ft.assert_greater_equal, 1, 2) - npt.assert_raises(AssertionError, ft.assert_not_equal, 5, 5) - npt.assert_raises(AssertionError, ft.assert_operator, 2, 1) + npt.assert_raises( + AssertionError, ft.assert_false, True, msg="True is not false", op=operator.eq + ) + npt.assert_raises( + AssertionError, ft.assert_true, False, msg="False is not true", op=operator.eq + ) + npt.assert_raises( + AssertionError, ft.assert_less, 2, 1, msg="{0} < {1}", op=operator.lt + ) + npt.assert_raises( + AssertionError, ft.assert_less_equal, 2, 1, msg="{0} =< {1}", op=operator.le + ) + npt.assert_raises( + AssertionError, ft.assert_greater, 1, 2, msg="{0} > {1}", op=operator.gt + ) + npt.assert_raises( + AssertionError, ft.assert_greater_equal, 1, 2, msg="{0} >= {1}", op=operator.ge + ) + npt.assert_raises(AssertionError, ft.assert_not_equal, 5, 5, msg="", op=operator.ne) + npt.assert_raises(AssertionError, ft.assert_operator, 2, 1, msg="", op=operator.eq) arr = [np.arange(k) for k in range(2, 12, 3)] arr2 = [np.arange(k) for k in range(2, 12, 4)] diff --git a/fury/tests/test_thread.py b/fury/tests/test_thread.py index 35e2ac041..8c26e30d4 100644 --- a/fury/tests/test_thread.py +++ b/fury/tests/test_thread.py @@ -20,7 +20,7 @@ def test_multithreading(): # Preparing the show manager as usual showm = window.ShowManager( - scene, size=(900, 768), reset_camera=False, order_transparent=True + scene=scene, size=(900, 768), reset_camera=False, order_transparent=True ) # showm.initialize() diff --git a/fury/tests/test_transform.py b/fury/tests/test_transform.py index 8c463592e..dfb260024 100644 --- a/fury/tests/test_transform.py +++ b/fury/tests/test_transform.py @@ -66,17 +66,17 @@ def test_sphere_cart(): def test_euler_matrix(): - rotation = euler_matrix(1, 2, 3, "syxz") + rotation = euler_matrix(1, 2, 3, axes="syxz") npt.assert_equal(np.allclose(np.sum(rotation[0]), -1.34786452), True) - rotation = euler_matrix(1, 2, 3, (0, 1, 0, 1)) + rotation = euler_matrix(1, 2, 3, axes=(0, 1, 0, 1)) npt.assert_equal(np.allclose(np.sum(rotation[0]), -0.383436184), True) ai, aj, ak = (4.0 * np.pi) * (np.random.random(3) - 0.5) for axes in _AXES2TUPLE.keys(): - _ = euler_matrix(ai, aj, ak, axes) + _ = euler_matrix(ai, aj, ak, axes=axes) for axes in _TUPLE2AXES.keys(): - _ = euler_matrix(ai, aj, ak, axes) + _ = euler_matrix(ai, aj, ak, axes=axes) def test_translate(): diff --git a/fury/tests/test_utils.py b/fury/tests/test_utils.py index 7fdc3230e..e7c347438 100644 --- a/fury/tests/test_utils.py +++ b/fury/tests/test_utils.py @@ -135,7 +135,7 @@ def test_polydata_lines(): line_2 = line_1 + np.array([0.5, 0.0, 0.0]) lines = [line_1, line_2] - pd_lines, is_cmap = utils.lines_to_vtk_polydata(lines, colors) + pd_lines, is_cmap = utils.lines_to_vtk_polydata(lines, colors=colors) res_lines = utils.get_polydata_lines(pd_lines) npt.assert_array_equal(lines, res_lines) npt.assert_equal(is_cmap, False) @@ -440,7 +440,14 @@ def test_numpy_to_vtk_image_data(): numpy_img_array = numpy_support.vtk_to_numpy(vtk_img_array) npt.assert_equal(np.flipud(array), numpy_img_array.reshape(h, w, elements)) - npt.assert_raises(IOError, utils.numpy_to_vtk_image_data, np.array([1, 2, 3])) + npt.assert_raises( + IOError, + utils.numpy_to_vtk_image_data, + np.array([1, 2, 3]), + spacing=(1, 1, 1), + origin=(0, 0, 0), + deep=True, + ) def test_get_grid_cell_position(): @@ -475,7 +482,7 @@ def test_rotate(interactive=False): rot = (90, 1, 0, 0) - rotate(act2, rot) + rotate(act2, rotation=rot) act3 = utils.shallow_copy(act) @@ -483,7 +490,7 @@ def test_rotate(interactive=False): rot = (90, 0, 1, 0) - rotate(act3, rot) + rotate(act3, rotation=rot) scene.add(act3) @@ -554,7 +561,7 @@ def test_vertices_from_actor(interactive=False): big_verts = res[0] big_faces = res[1] big_colors = res[2] - actr = get_actor_from_primitive(big_verts, big_faces, big_colors) + actr = get_actor_from_primitive(big_verts, big_faces, colors=big_colors) actr.GetProperty().BackfaceCullingOff() if interactive: scene = window.Scene() @@ -643,7 +650,14 @@ def test_get_actor_from_primitive(): vertices, triangles = fp.prim_frustum() colors = np.array([1, 0, 0]) npt.assert_raises( - ValueError, get_actor_from_primitive, vertices, triangles, colors=colors + ValueError, + get_actor_from_primitive, + vertices, + triangles, + colors=colors, + normals=None, + backface_culling=True, + prim_count=1, ) @@ -824,7 +838,7 @@ def test_color_check(): points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]]) colors = np.array([[1, 0, 0, 0.5], [0, 1, 0, 0.5], [0, 0, 1, 0.5]]) - color_tuple = color_check(len(points), colors) + color_tuple = color_check(len(points), colors=colors) color_array, global_opacity = color_tuple npt.assert_equal(color_array, np.floor(colors * 255)) @@ -833,7 +847,7 @@ def test_color_check(): points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]]) colors = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) - color_tuple = color_check(len(points), colors) + color_tuple = color_check(len(points), colors=colors) color_array, global_opacity = color_tuple npt.assert_equal(color_array, np.floor(colors * 255)) @@ -842,7 +856,7 @@ def test_color_check(): points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]]) colors = (1, 1, 1, 0.5) - color_tuple = color_check(len(points), colors) + color_tuple = color_check(len(points), colors=colors) color_array, global_opacity = color_tuple npt.assert_equal(color_array, np.floor(np.array([colors] * 3) * 255)) @@ -851,7 +865,7 @@ def test_color_check(): points = np.array([[0, 0, 0], [0, 1, 0], [1, 0, 0]]) colors = (1, 0, 0) - color_tuple = color_check(len(points), colors) + color_tuple = color_check(len(points), colors=colors) color_array, global_opacity = color_tuple npt.assert_equal(color_array, np.floor(np.array([colors] * 3) * 255)) @@ -878,12 +892,17 @@ def test_is_ui(): def test_empty_list_to_polydata(): lines = [[]] - npt.assert_raises(ValueError, utils.lines_to_vtk_polydata, lines) + npt.assert_raises( + ValueError, + utils.lines_to_vtk_polydata, + lines, + colors=None, + ) def test_empty_array_to_polydata(): lines = np.array([[]]) - npt.assert_raises(ValueError, utils.lines_to_vtk_polydata, lines) + npt.assert_raises(ValueError, utils.lines_to_vtk_polydata, lines, colors=None) @pytest.mark.skipif(not have_dipy, reason="Requires DIPY") @@ -891,7 +910,7 @@ def test_empty_array_sequence_to_polydata(): from dipy.tracking.streamline import Streamlines lines = Streamlines() - npt.assert_raises(ValueError, utils.lines_to_vtk_polydata, lines) + npt.assert_raises(ValueError, utils.lines_to_vtk_polydata, lines, colors=None) def test_set_polydata_primitives_count(): @@ -944,7 +963,7 @@ def test_set_actor_origin(): cube = actor.cube(np.array([[0, 0, 0]])) orig_vert = np.copy(vertices_from_actor(cube)) - utils.set_actor_origin(cube, np.array([0.5, 0.5, 0.5])) + utils.set_actor_origin(cube, center=np.array([0.5, 0.5, 0.5])) new_vert = np.copy(vertices_from_actor(cube)) npt.assert_array_equal(orig_vert, new_vert + np.array([0.5, 0.5, 0.5])) diff --git a/fury/tests/test_window.py b/fury/tests/test_window.py index 87115298e..3c3e11b4e 100644 --- a/fury/tests/test_window.py +++ b/fury/tests/test_window.py @@ -36,26 +36,26 @@ def test_scene(): axes = actor.axes() scene.add(axes) arr = window.snapshot(scene) - report = window.analyze_snapshot(arr, bg_color) + report = window.analyze_snapshot(arr, bg_color=bg_color) npt.assert_equal(report.objects, 1) # Test remove actor function by analyzing a snapshot scene.rm(axes) arr = window.snapshot(scene) - report = window.analyze_snapshot(arr, bg_color) + report = window.analyze_snapshot(arr, bg_color=bg_color) npt.assert_equal(report.objects, 0) # Add actor to scene to test the remove all actors function by analyzing a # snapshot scene.add(axes) arr = window.snapshot(scene) - report = window.analyze_snapshot(arr, bg_color) + report = window.analyze_snapshot(arr, bg_color=bg_color) npt.assert_equal(report.objects, 1) # Test remove all actors function by analyzing a snapshot scene.rm_all() arr = window.snapshot(scene) - report = window.analyze_snapshot(arr, bg_color) + report = window.analyze_snapshot(arr, bg_color=bg_color) npt.assert_equal(report.objects, 0) # Test change background color from scene by analyzing the scene - ren2 = window.Scene(bg_float) + ren2 = window.Scene(background=bg_float) ren2.background((0, 0, 0.0)) report = window.analyze_scene(ren2) npt.assert_equal(report.bg_color, (0, 0, 0)) @@ -130,7 +130,9 @@ def test_active_camera(): direction = scene.camera_direction() position, focal_point, view_up = scene.get_camera() - scene.set_camera((0.0, 0.0, 1.0), (0.0, 0.0, 0), view_up) + scene.set_camera( + position=(0.0, 0.0, 1.0), focal_point=(0.0, 0.0, 0), view_up=view_up + ) position, focal_point, view_up = scene.get_camera() npt.assert_almost_equal(np.dot(direction, position), -1) @@ -168,7 +170,9 @@ def test_active_camera(): report = window.analyze_snapshot(arr, colors=(0, 255, 0)) npt.assert_equal(report.colors_found, [True]) - scene.set_camera((0.0, 0.0, 1.0), (0.0, 0.0, 0), view_up) + scene.set_camera( + position=(0.0, 0.0, 1.0), focal_point=(0.0, 0.0, 0), view_up=view_up + ) # vertical rotation of the camera around the focal point scene.pitch(10) @@ -203,14 +207,14 @@ def test_parallel_projection(): # Put the camera on a angle so that the # camera can show the difference between perspective # and parallel projection - scene.set_camera((1.5, 1.5, 1.5)) + scene.set_camera(position=(1.5, 1.5, 1.5)) scene.GetActiveCamera().Zoom(2) # window.show(scene, reset_camera=True) scene.reset_camera() arr = window.snapshot(scene) - scene.projection("parallel") + scene.projection(proj_type="parallel") # window.show(scene, reset_camera=False) arr2 = window.snapshot(scene) # Because of the parallel projection the two axes @@ -218,7 +222,7 @@ def test_parallel_projection(): # pixels rather than in perspective projection were # the axes being further will be smaller. npt.assert_equal(np.sum(arr2 > 0) > np.sum(arr > 0), True) - scene.projection("perspective") + scene.projection(proj_type="perspective") arr2 = window.snapshot(scene) npt.assert_equal(np.sum(arr2 > 0), np.sum(arr > 0)) @@ -322,7 +326,7 @@ def test_save_screenshot(): scene.add(sphere_actor) window_sz = (400, 400) - show_m = window.ShowManager(scene, size=window_sz) + show_m = window.ShowManager(scene=scene, size=window_sz) with InTemporaryDirectory(): fname = "test.png" @@ -359,7 +363,7 @@ def test_stereo(): np.array([[-1, 1, 0.0], [1, 1, 0.0]]), ] colors = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) - stream_actor = actor.streamtube(lines, colors, linewidth=0.3, opacity=0.5) + stream_actor = actor.streamtube(lines, colors=colors, linewidth=0.3, opacity=0.5) scene.add(stream_actor) @@ -398,7 +402,7 @@ def test_frame_rate(): scene.add(sphere_actor) showm = window.ShowManager( - scene, size=(900, 768), reset_camera=False, order_transparent=True + scene=scene, size=(900, 768), reset_camera=False, order_transparent=True ) counter = itertools.count() @@ -540,7 +544,7 @@ def test_opengl_state_simple(): scales=0.2, ) showm = window.ShowManager( - scene, size=(900, 768), reset_camera=False, order_transparent=False + scene=scene, size=(900, 768), reset_camera=False, order_transparent=False ) scene.add(actors) @@ -566,7 +570,7 @@ def test_opengl_state_add_remove_and_check(): scales=0.2, ) showm = window.ShowManager( - scene, size=(900, 768), reset_camera=False, order_transparent=False + scene=scene, size=(900, 768), reset_camera=False, order_transparent=False ) scene.add(actor_no_depth_test) diff --git a/fury/transform.py b/fury/transform.py index ac9c1aba8..be7b9fd6a 100644 --- a/fury/transform.py +++ b/fury/transform.py @@ -3,6 +3,8 @@ import numpy as np from scipy.spatial.transform import Rotation as Rot # type: ignore +from fury.decorators import warn_on_args_to_kwargs + # axis sequences for Euler angles _NEXT_AXIS = [1, 2, 0, 1] @@ -37,7 +39,8 @@ _TUPLE2AXES = {v: k for k, v in _AXES2TUPLE.items()} -def euler_matrix(ai, aj, ak, axes="sxyz"): +@warn_on_args_to_kwargs() +def euler_matrix(ai, aj, ak, *, axes="sxyz"): """Return homogeneous rotation matrix from Euler angles and axis sequence. Code modified from the work of Christoph Gohlke link provided here @@ -252,7 +255,10 @@ def translate(translation): iden = np.identity(4) translation = np.append(translation, 0).reshape(-1, 1) - t = np.array([[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]], np.float32) + t = np.array( + [[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]], + np.float32, + ) translation = np.multiply(t, translation) translation = np.add(iden, translation) diff --git a/fury/ui/core.py b/fury/ui/core.py index 5507a613a..6ced09f59 100644 --- a/fury/ui/core.py +++ b/fury/ui/core.py @@ -175,7 +175,9 @@ def add_to_scene(self, scene): if callback[0] == self._scene: iren.add_callback(iren, callback[1], callback[2], args=[self]) else: - iren.add_callback(*callback, args=[self]) + # iren.add_callback(*callback, args=[self]) + if len(callback) > 3: + iren.add_callback(*callback[:3], priority=callback[3], args=[self]) def add_callback(self, prop, event_type, callback, priority=0): """Add a callback to a specific event for this UI component. diff --git a/fury/ui/helpers.py b/fury/ui/helpers.py index 8a9c39de5..939519a27 100644 --- a/fury/ui/helpers.py +++ b/fury/ui/helpers.py @@ -2,10 +2,13 @@ import numpy as np +from fury.decorators import warn_on_args_to_kwargs + TWO_PI = 2 * np.pi -def clip_overflow(textblock, width, side="right"): +@warn_on_args_to_kwargs() +def clip_overflow(textblock, width, *, side="right"): """Clips overflowing text of TextBlock2D with respect to width. Parameters @@ -27,7 +30,7 @@ def clip_overflow(textblock, width, side="right"): original_str = textblock.message prev_bg = textblock.have_bg - clip_idx = check_overflow(textblock, width, "...", side) + clip_idx = check_overflow(textblock, width, overflow_postfix="...", side=side) if clip_idx == 0: return original_str @@ -36,7 +39,8 @@ def clip_overflow(textblock, width, side="right"): return textblock.message -def wrap_overflow(textblock, wrap_width, side="right"): +@warn_on_args_to_kwargs() +def wrap_overflow(textblock, wrap_width, *, side="right"): """Wraps overflowing text of TextBlock2D with respect to width. Parameters @@ -59,7 +63,7 @@ def wrap_overflow(textblock, wrap_width, side="right"): str_copy = textblock.message wrap_idxs = [] - wrap_idx = check_overflow(textblock, wrap_width, "", side) + wrap_idx = check_overflow(textblock, wrap_width, overflow_postfix="", side=side) if wrap_idx == 0: return original_str @@ -69,7 +73,7 @@ def wrap_overflow(textblock, wrap_width, side="right"): while wrap_idx != 0: str_copy = str_copy[wrap_idx:] textblock.message = str_copy - wrap_idx = check_overflow(textblock, wrap_width, "", side) + wrap_idx = check_overflow(textblock, wrap_width, overflow_postfix="", side=side) if wrap_idx != 0: wrap_idxs.append(wrap_idxs[-1] + wrap_idx + 1) @@ -80,7 +84,8 @@ def wrap_overflow(textblock, wrap_width, side="right"): return textblock.message -def check_overflow(textblock, width, overflow_postfix="", side="right"): +@warn_on_args_to_kwargs() +def check_overflow(textblock, width, *, overflow_postfix="", side="right"): """Checks if the text is overflowing. Parameters diff --git a/fury/ui/tests/test_containers.py b/fury/ui/tests/test_containers.py index 333fb7b44..1c016290d 100644 --- a/fury/ui/tests/test_containers.py +++ b/fury/ui/tests/test_containers.py @@ -22,7 +22,7 @@ def setup_module(): def test_wrong_interactor_style(): panel = ui.Panel2D(size=(300, 150)) dummy_scene = window.Scene() - _ = window.ShowManager(dummy_scene, interactor_style="trackball") + _ = window.ShowManager(scene=dummy_scene, interactor_style="trackball") npt.assert_raises(TypeError, panel.add_to_scene, dummy_scene) @@ -35,17 +35,32 @@ def test_grid_ui1(interactive=False): vol1[25:75, 25:75, 25:75] = 100 colors = distinguishable_colormap(nb_colors=3) - contour_actor1 = actor.contour_from_roi(vol1, np.eye(4), colors[0], 1.0) + contour_actor1 = actor.contour_from_roi( + vol1, + affine=np.eye(4), + color=colors[0], + opacity=1.0, + ) vol2 = np.zeros((100, 100, 100)) vol2[25:75, 25:75, 25:75] = 100 - contour_actor2 = actor.contour_from_roi(vol2, np.eye(4), colors[1], 1.0) + contour_actor2 = actor.contour_from_roi( + vol2, + affine=np.eye(4), + color=colors[1], + opacity=1.0, + ) vol3 = np.zeros((100, 100, 100)) vol3[25:75, 25:75, 25:75] = 100 - contour_actor3 = actor.contour_from_roi(vol3, np.eye(4), colors[2], 1.0) + contour_actor3 = actor.contour_from_roi( + vol3, + affine=np.eye(4), + color=colors[2], + opacity=1.0, + ) scene = window.Scene() actors = [] @@ -88,7 +103,7 @@ def test_grid_ui1(interactive=False): texts.append(text_actor3) counter = itertools.count() - show_m = window.ShowManager(scene) + show_m = window.ShowManager(scene=scene) def timer_callback(_obj, _event): nonlocal show_m, counter @@ -136,17 +151,32 @@ def test_grid_ui2(interactive=False): vol1[25:75, 25:75, 25:75] = 100 colors = distinguishable_colormap(nb_colors=3) - contour_actor1 = actor.contour_from_roi(vol1, np.eye(4), colors[0], 1.0) + contour_actor1 = actor.contour_from_roi( + vol1, + affine=np.eye(4), + color=colors[0], + opacity=1.0, + ) vol2 = np.zeros((100, 100, 100)) vol2[25:75, 25:75, 25:75] = 100 - contour_actor2 = actor.contour_from_roi(vol2, np.eye(4), colors[1], 1.0) + contour_actor2 = actor.contour_from_roi( + vol2, + affine=np.eye(4), + color=colors[1], + opacity=1.0, + ) vol3 = np.zeros((100, 100, 100)) vol3[25:75, 25:75, 25:75] = 100 - contour_actor3 = actor.contour_from_roi(vol3, np.eye(4), colors[2], 1.0) + contour_actor3 = actor.contour_from_roi( + vol3, + affine=np.eye(4), + color=colors[2], + opacity=1.0, + ) scene = window.Scene() actors = [] @@ -198,7 +228,9 @@ def test_grid_ui2(interactive=False): current_size = (900, 600) scene = window.Scene() - show_manager = window.ShowManager(scene, size=current_size, title="FURY GridUI") + show_manager = window.ShowManager( + scene=scene, size=current_size, title="FURY GridUI" + ) grid_ui2 = ui.GridUI( actors=actors, diff --git a/fury/ui/tests/test_elements.py b/fury/ui/tests/test_elements.py index 7df1645b1..2f72949cc 100644 --- a/fury/ui/tests/test_elements.py +++ b/fury/ui/tests/test_elements.py @@ -1171,7 +1171,7 @@ def test_ui_draw_shape(): current_size = (900, 900) scene = window.Scene() show_manager = window.ShowManager( - scene, size=current_size, title="DrawShape UI Example" + scene=scene, size=current_size, title="DrawShape UI Example" ) scene.add(line, circle, quad) diff --git a/fury/ui/tests/test_elements_callback.py b/fury/ui/tests/test_elements_callback.py index 89c6aa506..4fa62e34b 100644 --- a/fury/ui/tests/test_elements_callback.py +++ b/fury/ui/tests/test_elements_callback.py @@ -51,7 +51,7 @@ def test_frame_rate_and_anti_aliasing(): counter = itertools.count() showm = window.ShowManager( - scene, + scene=scene, size=(1980, 1080), reset_camera=False, order_transparent=True, @@ -103,7 +103,7 @@ def timer_callback(_obj, _event): counter = itertools.count() multi_samples = 0 showm = window.ShowManager( - scene, + scene=scene, size=(1980, 1080), reset_camera=False, order_transparent=True, @@ -145,7 +145,7 @@ def test_timer(): sphere_actor = actor.sphere(centers=xyzr[:, :3], colors=colors[:], radii=xyzr[:, 3]) - vertices, faces = prim_sphere("repulsion724") + vertices, faces = prim_sphere(name="repulsion724") sphere_actor2 = actor.sphere( centers=xyzr2[:, :3], @@ -161,7 +161,10 @@ def test_timer(): tb = ui.TextBlock2D() counter = itertools.count() showm = window.ShowManager( - scene, size=(1024, 768), reset_camera=False, order_transparent=True + scene=scene, + size=(1024, 768), + reset_camera=False, + order_transparent=True, ) scene.add(tb) diff --git a/fury/ui/tests/test_helpers.py b/fury/ui/tests/test_helpers.py index 039d28d83..34f7c1039 100644 --- a/fury/ui/tests/test_helpers.py +++ b/fury/ui/tests/test_helpers.py @@ -32,26 +32,26 @@ def test_clip_overflow(): npt.assert_equal("A ...", text.message) text.message = "Hello" - clip_overflow(text, text.size[0], "left") + clip_overflow(text, text.size[0], side="left") npt.assert_equal("Hello", text.message) text.message = "Hello wassup" - clip_overflow(text, text.size[0], "left") + clip_overflow(text, text.size[0], side="left") npt.assert_equal("...up", text.message) text.message = "A very very long message to clip text overflow" - clip_overflow(text, text.size[0], "left") + clip_overflow(text, text.size[0], side="left") npt.assert_equal("...ow", text.message) text.message = "A very very long message to clip text overflow" - clip_overflow(text, text.size[0], "LeFT") + clip_overflow(text, text.size[0], side="LeFT") npt.assert_equal("...ow", text.message) text.message = "A very very long message to clip text overflow" - clip_overflow(text, text.size[0], "RigHT") + clip_overflow(text, text.size[0], side="RigHT") npt.assert_equal("A ...", text.message) - npt.assert_raises(ValueError, clip_overflow, text, text.size[0], "middle") + npt.assert_raises(ValueError, clip_overflow, text, text.size[0], side="middle") def test_wrap_overflow(): @@ -97,7 +97,7 @@ def test_check_overflow(): text.message = "A very very long message to clip text overflow" - overflow_idx = check_overflow(text, 100, "~") + overflow_idx = check_overflow(text, 100, overflow_postfix="~") npt.assert_equal(4, overflow_idx) npt.assert_equal("A ve~", text.message) diff --git a/fury/utils.py b/fury/utils.py index 12aeb2214..7c269ea5a 100644 --- a/fury/utils.py +++ b/fury/utils.py @@ -2,6 +2,7 @@ from scipy.ndimage import map_coordinates from fury.colormap import line_colors +from fury.decorators import warn_on_args_to_kwargs from fury.lib import ( VTK_DOUBLE, VTK_FLOAT, @@ -116,7 +117,8 @@ def numpy_to_vtk_colors(colors): return vtk_colors -def numpy_to_vtk_cells(data, is_coords=True): +@warn_on_args_to_kwargs() +def numpy_to_vtk_cells(data, *, is_coords=True): """Convert numpy array to a vtk cell array. Parameters @@ -172,8 +174,9 @@ def numpy_to_vtk_cells(data, is_coords=True): return cell_array +@warn_on_args_to_kwargs() def numpy_to_vtk_image_data( - array, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0), deep=True + array, *, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0), deep=True ): """Convert numpy array to a vtk image data. @@ -243,7 +246,8 @@ def map_coordinates_3d_4d(input_array, indices): return np.ascontiguousarray(np.array(values_4d).T) -def lines_to_vtk_polydata(lines, colors=None): +@warn_on_args_to_kwargs() +def lines_to_vtk_polydata(lines, *, colors=None): """Create a vtkPolyData with lines and colors. Parameters @@ -501,7 +505,8 @@ def get_polydata_colors(polydata): return numpy_support.vtk_to_numpy(vtk_colors) -def get_polydata_field(polydata, field_name, as_vtk=False): +@warn_on_args_to_kwargs() +def get_polydata_field(polydata, field_name, *, as_vtk=False): """Get a field from a vtk polydata. Parameters @@ -526,7 +531,8 @@ def get_polydata_field(polydata, field_name, as_vtk=False): return numpy_support.vtk_to_numpy(vtk_field_data) -def add_polydata_numeric_field(polydata, field_name, field_data, array_type=VTK_INT): +@warn_on_args_to_kwargs() +def add_polydata_numeric_field(polydata, field_name, field_data, *, array_type=VTK_INT): """Add a field to a vtk polydata. Parameters @@ -668,7 +674,8 @@ def set_polydata_tangents(polydata, tangents): return polydata -def set_polydata_colors(polydata, colors, array_name="colors"): +@warn_on_args_to_kwargs() +def set_polydata_colors(polydata, colors, *, array_name="colors"): """Set polydata colors with a numpy array (ndarrays Nx3 int). Parameters @@ -781,8 +788,15 @@ def get_actor_from_polydata(polydata): return get_actor_from_polymapper(poly_mapper) +@warn_on_args_to_kwargs() def get_actor_from_primitive( - vertices, triangles, colors=None, normals=None, backface_culling=True, prim_count=1 + vertices, + triangles, + *, + colors=None, + normals=None, + backface_culling=True, + prim_count=1, ): """Get actor from a vtkPolyData. @@ -831,9 +845,11 @@ def get_actor_from_primitive( return current_actor +@warn_on_args_to_kwargs() def repeat_sources( centers, colors, + *, active_scalars=1.0, directions=None, source=None, @@ -1047,7 +1063,8 @@ def get_bounding_box_sizes(actor): return (X2 - X1, Y2 - Y1, Z2 - Z1) -def get_grid_cells_position(shapes, aspect_ratio=16 / 9.0, dim=None): +@warn_on_args_to_kwargs() +def get_grid_cells_position(shapes, *, aspect_ratio=16 / 9.0, dim=None): """Construct a XY-grid based on the cells content shape. This function generates the coordinates of every grid cell. The width and @@ -1103,7 +1120,8 @@ def shallow_copy(vtk_object): return copy -def rotate(actor, rotation=(90, 1, 0, 0)): +@warn_on_args_to_kwargs() +def rotate(actor, *, rotation=(90, 1, 0, 0)): """Rotate actor around axis by angle. Parameters @@ -1286,7 +1304,8 @@ def change_vertices_order(triangle): return np.array([triangle[2], triangle[1], triangle[0]]) -def fix_winding_order(vertices, triangles, clockwise=False): +@warn_on_args_to_kwargs() +def fix_winding_order(vertices, triangles, *, clockwise=False): """Return corrected triangles. Given an ordering of the triangle's three vertices, a triangle can appear @@ -1318,7 +1337,8 @@ def fix_winding_order(vertices, triangles, clockwise=False): return corrected_triangles -def vertices_from_actor(actor, as_vtk=False): +@warn_on_args_to_kwargs() +def vertices_from_actor(actor, *, as_vtk=False): """Access to vertices from actor. Parameters @@ -1339,7 +1359,8 @@ def vertices_from_actor(actor, as_vtk=False): return numpy_support.vtk_to_numpy(vtk_array) -def colors_from_actor(actor, array_name="colors", as_vtk=False): +@warn_on_args_to_kwargs() +def colors_from_actor(actor, *, array_name="colors", as_vtk=False): """Access colors from actor which uses polydata. Parameters @@ -1392,7 +1413,8 @@ def tangents_from_actor(act): return get_polydata_tangents(polydata) -def array_from_actor(actor, array_name, as_vtk=False): +@warn_on_args_to_kwargs() +def array_from_actor(actor, array_name, *, as_vtk=False): """Access array from actor which uses polydata. Parameters @@ -1459,7 +1481,8 @@ def compute_bounds(actor): actor.GetMapper().GetInput().ComputeBounds() -def update_actor(actor, all_arrays=True): +@warn_on_args_to_kwargs() +def update_actor(actor, *, all_arrays=True): """Update actor. Parameters @@ -1524,7 +1547,8 @@ def update_surface_actor_colors(actor, colors): ) -def color_check(pts_len, colors=None): +@warn_on_args_to_kwargs() +def color_check(pts_len, *, colors=None): """Returns a VTK scalar array containing colors information for each one of the points according to the policy defined by the parameter colors. @@ -1580,7 +1604,8 @@ def is_ui(actor): return all(hasattr(actor, attr) for attr in ["add_to_scene", "_setup"]) -def set_actor_origin(actor, center=None): +@warn_on_args_to_kwargs() +def set_actor_origin(actor, *, center=None): """Change the origin of an actor to a custom position. Parameters diff --git a/fury/window.py b/fury/window.py index 7c36cedf3..fc79285fa 100644 --- a/fury/window.py +++ b/fury/window.py @@ -10,6 +10,7 @@ from fury import __version__ as fury_version import fury.animation as anim +from fury.decorators import warn_on_args_to_kwargs from fury.interactor import CustomInteractorStyle from fury.io import load_image, save_image from fury.lib import ( @@ -47,7 +48,8 @@ class Scene(OpenGLRenderer): available in ``vtkRenderer`` if necessary. """ - def __init__(self, background=(0, 0, 0), skybox=None): + @warn_on_args_to_kwargs() + def __init__(self, *, background=(0, 0, 0), skybox=None): self.__skybox = skybox self.__skybox_actor = None if skybox: @@ -61,7 +63,8 @@ def background(self, color): """Set a background color.""" self.SetBackground(color) - def skybox(self, visible=True, gamma_correct=True): + @warn_on_args_to_kwargs() + def skybox(self, *, visible=True, gamma_correct=True): """Show or hide the skybox. Parameters @@ -112,7 +115,8 @@ def rm_all(self): """Remove all actors from the scene.""" self.RemoveAllViewProps() - def projection(self, proj_type="perspective"): + @warn_on_args_to_kwargs() + def projection(self, *, proj_type="perspective"): """Decide between parallel or perspective projection. Parameters @@ -130,7 +134,8 @@ def reset_camera(self): """Reset the camera to an automatic position given by the engine.""" self.ResetCamera() - def reset_camera_tight(self, margin_factor=1.02): + @warn_on_args_to_kwargs() + def reset_camera_tight(self, *, margin_factor=1.02): """Resets camera so the content fit tightly within the window. Parameters @@ -180,7 +185,8 @@ def camera_info(self): print(" Focal Point (%.2f, %.2f, %.2f)" % cam.GetFocalPoint()) print(" View Up (%.2f, %.2f, %.2f)" % cam.GetViewUp()) - def set_camera(self, position=None, focal_point=None, view_up=None): + @warn_on_args_to_kwargs() + def set_camera(self, *, position=None, focal_point=None, view_up=None): """Set up camera position / Focal Point / View Up.""" if position is not None: self.GetActiveCamera().SetPosition(*position) @@ -290,8 +296,10 @@ def fxaa_off(self): class ShowManager: """Class interface between the scene, the window and the interactor.""" + @warn_on_args_to_kwargs() def __init__( self, + *, scene=None, title="FURY", size=(300, 300), @@ -521,7 +529,8 @@ def is_done(self): except AttributeError: return True - def start(self, multithreaded=False, desired_fps=60): + @warn_on_args_to_kwargs() + def start(self, *, multithreaded=False, desired_fps=60): """Start interaction. Parameters @@ -666,7 +675,8 @@ def _stop_recording_and_close(_obj, _evt): events = f.read() return events - def record_events_to_file(self, filename="record.log"): + @warn_on_args_to_kwargs() + def record_events_to_file(self, *, filename="record.log"): """Record events during the interaction. The recording is represented as a list of VTK events @@ -744,7 +754,8 @@ def play_events_from_file(self, filename): self.play_events(events) - def add_window_callback(self, win_callback, event=Command.ModifiedEvent): + @warn_on_args_to_kwargs() + def add_window_callback(self, win_callback, *, event=Command.ModifiedEvent): """Add window callbacks.""" self.window.AddObserver(event, win_callback) self.window.Render() @@ -761,7 +772,8 @@ def add_timer_callback(self, repeat, duration, timer_callback): self.timers.append(timer_id) return timer_id - def add_iren_callback(self, iren_callback, event="MouseMoveEvent"): + @warn_on_args_to_kwargs() + def add_iren_callback(self, iren_callback, *, event="MouseMoveEvent"): if not self.iren.GetInitialized(): self.initialize() self.iren.AddObserver(event, iren_callback) @@ -784,7 +796,8 @@ def exit(self): self.destroy_timers() self.timers.clear() - def save_screenshot(self, fname, magnification=1, size=None, stereo=None): + @warn_on_args_to_kwargs() + def save_screenshot(self, fname, *, magnification=1, size=None, stereo=None): """Save a screenshot of the current window in the specified filename. Parameters @@ -829,8 +842,10 @@ def save_screenshot(self, fname, magnification=1, size=None, stereo=None): ) +@warn_on_args_to_kwargs() def show( scene, + *, title="FURY", size=(300, 300), png_magnify=1, @@ -904,12 +919,12 @@ def show( """ show_manager = ShowManager( - scene, - title, - size, - png_magnify, - reset_camera, - order_transparent, + scene=scene, + title=title, + size=size, + png_magnify=png_magnify, + reset_camera=reset_camera, + order_transparent=order_transparent, stereo=stereo, multi_samples=multi_samples, max_peels=max_peels, @@ -919,7 +934,9 @@ def show( show_manager.start() +@warn_on_args_to_kwargs() def record( + *, scene=None, cam_pos=None, cam_focal=None, @@ -1073,7 +1090,8 @@ def record( renWin.Finalize() -def antialiasing(scene, win, multi_samples=8, max_peels=4, occlusion_ratio=0.0): +@warn_on_args_to_kwargs() +def antialiasing(scene, win, *, multi_samples=8, max_peels=4, occlusion_ratio=0.0): """Enable anti-aliasing and ordered transparency. Parameters @@ -1116,8 +1134,10 @@ def antialiasing(scene, win, multi_samples=8, max_peels=4, occlusion_ratio=0.0): scene.SetOcclusionRatio(occlusion_ratio) +@warn_on_args_to_kwargs() def snapshot( scene, + *, fname=None, size=(300, 300), offscreen=True, @@ -1190,7 +1210,11 @@ def snapshot( if order_transparent: antialiasing( - scene, render_window, multi_samples, max_peels, occlusion_ratio + scene, + render_window, + multi_samples=multi_samples, + max_peels=max_peels, + occlusion_ratio=occlusion_ratio, ) render_window.Render() @@ -1238,8 +1262,9 @@ class ReportScene: return report +@warn_on_args_to_kwargs() def analyze_snapshot( - im, bg_color=colors.black, colors=None, find_objects=True, strel=None + im, *, bg_color=colors.black, colors=None, find_objects=True, strel=None ): """Analyze snapshot from memory or file.