commit 7385f9bb9b1938959e880716d7237dc8b07f9087 Author: HowToSD Date: Mon Mar 10 14:34:01 2025 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9242fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +**/__pycache__/ +models/ +*.pth +/datasets/ +examples/datasets/.~lock* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4667c3a --- /dev/null +++ b/LICENSE @@ -0,0 +1,41 @@ +ComfyUI-Data-Analysis License + +Copyright (c) 2025 Hideyuki Inada + +Permission is hereby granted, free of charge, to individuals and academic institutions to use and modify this software for **non-commercial, personal, and academic purposes only**, subject to the following conditions: + +### 1. **No Commercial Use** +This software **may not be used, directly or indirectly, by any business, commercial entity, for-profit organization, or any individual acting on behalf of such an entity, without prior written permission from the author**. + +**Commercial use is strictly prohibited and includes, but is not limited to:** +- Using the software as part of any business operations, service offerings, or products. +- Using the software for any work, research, or analysis conducted **for a company, client, or commercial entity**, even if the user is an individual. +- Using the software in any project that generates revenue, whether directly or indirectly. +- Integrating the software into proprietary or closed-source applications, whether internal or external to an organization. +- Distributing, sublicensing, or embedding this software in any product, tool, or system used in a commercial setting. + +**Clarification on Individual vs. Commercial Use:** +- **Personal use is allowed**: Individuals may use the software for their own personal, educational, or research purposes as long as they are not compensated for their use. +- **Work-related use is prohibited**: If an individual is using the software **in connection with their employment, contract work, or professional activities for a commercial entity**, such use is considered commercial and requires a separate paid license. +- **Academic use is allowed**: Students, educators, and researchers may use the software for non-commercial academic purposes, provided they are not engaging in research that is sponsored, commissioned, or funded by a commercial entity. + +### 2. **No Redistribution** +Redistribution of this software, in any form, is strictly prohibited without explicit written permission from the author. This includes, but is not limited to: +- Sharing, publishing, or hosting this software on any platform or repository. +- Modifying and distributing the software, even if the modifications are minor. +- Embedding or bundling this software with any other package, tool, or system. + +### 3. **No Warranty & Limitation of Liability** +THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. + +IN NO EVENT SHALL THE AUTHOR(S) OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, LOSSES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +THE USER ACKNOWLEDGES THAT THIS SOFTWARE IS INTENDED FOR DATA ANALYSIS AND MAY CONTAIN ERRORS OR PRODUCE INCORRECT RESULTS. THE AUTHOR(S) DISCLAIM ANY LIABILITY FOR DAMAGES, LOSSES, OR CONSEQUENCES ARISING FROM RELIANCE ON THE SOFTWARE'S OUTPUT, INCLUDING BUT NOT LIMITED TO **INCORRECT ANALYSIS, FAULTY DATA INTERPRETATION, BUSINESS LOSSES, OR LEGAL CONSEQUENCES.** + +THE USER ACCEPTS FULL RESPONSIBILITY FOR VERIFYING THE ACCURACY AND RELIABILITY OF ANY RESULTS GENERATED BY THIS SOFTWARE. + +### 4. **Governing Law** +This License shall be governed and interpreted under the laws of the State of California, U.S.A., without regard to conflict of law principles. + +For commercial licensing inquiries, contact: support@candee.ai + diff --git a/README.md b/README.md new file mode 100644 index 0000000..10dcf70 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# ComfyUI-Pt-Wrapper + +ComfyUI-Pt-Wrapper is a spin-off of [ComfyUI-Data-Analysis](https://github.com/HowToSD/ComfyUI-Data-Analysis), created to provide a dedicated extension for PyTorch wrapper nodes originally included in the analysis extension. + +This extension offers an extensive collection of nodes for direct operations on PyTorch tensors, along with specialized nodes for machine learning tasks such as ResNet. + +For instance, you can train an image classification model using your own image data **without writing any code**. + +Check out [this tutorial on classifying dogs and cats](docs/dog_cat_classification_model_training.md). + +If you need visualization or analysis features, install ComfyUI-Data-Analysis as well. + +## Contribution +🚫 **This project does not accept pull requests.** +Unsolicited PRs will be **closed without review**. + +If you have a feature request or improvement suggestion, **please create an Issue instead**. +All submitted issues will be reviewed and prioritized accordingly. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3010ba7 --- /dev/null +++ b/__init__.py @@ -0,0 +1,370 @@ +# This file is auto-generated. Do not edit manually. +from typing import Dict, Type, TypeVar +from .modules.pytorch_wrapper.pt_abs import PtAbs +from .modules.pytorch_wrapper.pt_acos import PtAcos +from .modules.pytorch_wrapper.pt_add import PtAdd +from .modules.pytorch_wrapper.pt_arange import PtArange +from .modules.pytorch_wrapper.pt_argmax import PtArgmax +from .modules.pytorch_wrapper.pt_argmin import PtArgmin +from .modules.pytorch_wrapper.pt_asin import PtAsin +from .modules.pytorch_wrapper.pt_atan import PtAtan +from .modules.pytorch_wrapper.pt_bitwise_and import PtBitwiseAnd +from .modules.pytorch_wrapper.pt_bitwise_left_shift import PtBitwiseLeftShift +from .modules.pytorch_wrapper.pt_bitwise_not import PtBitwiseNot +from .modules.pytorch_wrapper.pt_bitwise_or import PtBitwiseOr +from .modules.pytorch_wrapper.pt_bitwise_right_shift import PtBitwiseRightShift +from .modules.pytorch_wrapper.pt_bitwise_xor import PtBitwiseXor +from .modules.pytorch_wrapper.pt_bmm import PtBmm +from .modules.pytorch_wrapper.pt_bool_create import PtBoolCreate +from .modules.pytorch_wrapper.pt_cos import PtCos +from .modules.pytorch_wrapper.pt_cosh import PtCosh +from .modules.pytorch_wrapper.pt_crop import PtCrop +from .modules.pytorch_wrapper.pt_data_loader import PtDataLoader +from .modules.pytorch_wrapper.pt_div import PtDiv +from .modules.pytorch_wrapper.pt_einsum import PtEinsum +from .modules.pytorch_wrapper.pt_eq import PtEq +from .modules.pytorch_wrapper.pt_evaluate_classification_model import PtEvaluateClassificationModel +from .modules.pytorch_wrapper.pt_exp import PtExp +from .modules.pytorch_wrapper.pt_flatten import PtFlatten +from .modules.pytorch_wrapper.pt_float_create import PtFloatCreate +from .modules.pytorch_wrapper.pt_floor_divide import PtFloorDiv +from .modules.pytorch_wrapper.pt_from_image import PtFromImage +from .modules.pytorch_wrapper.pt_from_image_transpose import PtFromImageTranspose +from .modules.pytorch_wrapper.pt_from_latent import PtFromLatent +from .modules.pytorch_wrapper.pt_from_numpy import PtFromNumpy +from .modules.pytorch_wrapper.pt_full import PtFull +from .modules.pytorch_wrapper.pt_gather import PtGather +from .modules.pytorch_wrapper.pt_ge import PtGe +from .modules.pytorch_wrapper.pt_gt import PtGt +from .modules.pytorch_wrapper.pt_index_select import PtIndexSelect +from .modules.pytorch_wrapper.pt_int_create import PtIntCreate +from .modules.pytorch_wrapper.pt_interpolate_by_scale_factor import PtInterpolateByScaleFactor +from .modules.pytorch_wrapper.pt_interpolate_to_size import PtInterpolateToSize +from .modules.pytorch_wrapper.pt_le import PtLe +from .modules.pytorch_wrapper.pt_linspace import PtLinspace +from .modules.pytorch_wrapper.pt_load_model import PtLoadModel +from .modules.pytorch_wrapper.pt_log import PtLog +from .modules.pytorch_wrapper.pt_logical_and import PtLogicalAnd +from .modules.pytorch_wrapper.pt_logical_not import PtLogicalNot +from .modules.pytorch_wrapper.pt_logical_or import PtLogicalOr +from .modules.pytorch_wrapper.pt_logical_xor import PtLogicalXor +from .modules.pytorch_wrapper.pt_lt import PtLt +from .modules.pytorch_wrapper.pt_masked_select import PtMaskedSelect +from .modules.pytorch_wrapper.pt_matmul import PtMatMul +from .modules.pytorch_wrapper.pt_max import PtMax +from .modules.pytorch_wrapper.pt_mean import PtMean +from .modules.pytorch_wrapper.pt_median import PtMedian +from .modules.pytorch_wrapper.pt_min import PtMin +from .modules.pytorch_wrapper.pt_mm import PtMm +from .modules.pytorch_wrapper.pt_mul import PtMul +from .modules.pytorch_wrapper.pt_ne import PtNe +from .modules.pytorch_wrapper.pt_neg import PtNeg +from .modules.pytorch_wrapper.pt_ones import PtOnes +from .modules.pytorch_wrapper.pt_pad import PtPad +from .modules.pytorch_wrapper.pt_permute import PtPermute +from .modules.pytorch_wrapper.pt_pow import PtPow +from .modules.pytorch_wrapper.pt_predict_classification_model import PtPredictClassificationModel +from .modules.pytorch_wrapper.pt_prod import PtProd +from .modules.pytorch_wrapper.pt_rand import PtRand +from .modules.pytorch_wrapper.pt_rand_int import PtRandInt +from .modules.pytorch_wrapper.pt_randn import PtRandn +from .modules.pytorch_wrapper.pt_remainder import PtRemainder +from .modules.pytorch_wrapper.pt_reshape import PtReshape +from .modules.pytorch_wrapper.pt_save_model import PtSaveModel +from .modules.pytorch_wrapper.pt_scatter import PtScatter +from .modules.pytorch_wrapper.pt_show_size import PtShowSize +from .modules.pytorch_wrapper.pt_show_text import PtShowText +from .modules.pytorch_wrapper.pt_sin import PtSin +from .modules.pytorch_wrapper.pt_sinh import PtSinh +from .modules.pytorch_wrapper.pt_size import PtSize +from .modules.pytorch_wrapper.pt_size_create import PtSizeCreate +from .modules.pytorch_wrapper.pt_size_to_numpy import PtSizeToNumpy +from .modules.pytorch_wrapper.pt_size_to_string import PtSizeToString +from .modules.pytorch_wrapper.pt_sqrt import PtSqrt +from .modules.pytorch_wrapper.pt_squeeze import PtSqueeze +from .modules.pytorch_wrapper.pt_std import PtStd +from .modules.pytorch_wrapper.pt_sub import PtSub +from .modules.pytorch_wrapper.pt_sum import PtSum +from .modules.pytorch_wrapper.pt_tan import PtTan +from .modules.pytorch_wrapper.pt_tanh import PtTanh +from .modules.pytorch_wrapper.pt_to_image import PtToImage +from .modules.pytorch_wrapper.pt_to_image_transpose import PtToImageTranspose +from .modules.pytorch_wrapper.pt_to_latent import PtToLatent +from .modules.pytorch_wrapper.pt_to_numpy import PtToNumpy +from .modules.pytorch_wrapper.pt_to_rgb_tensors import PtToRgbTensors +from .modules.pytorch_wrapper.pt_train_classification_model import PtTrainClassificationModel +from .modules.pytorch_wrapper.pt_unsqueeze import PtUnsqueeze +from .modules.pytorch_wrapper.pt_var import PtVar +from .modules.pytorch_wrapper.pt_view import PtView +from .modules.pytorch_wrapper.pt_where import PtWhere +from .modules.pytorch_wrapper.pt_zeros import PtZeros +from .modules.pytorch_wrapper.ptn_conv_model import PtnConvModel +from .modules.pytorch_wrapper.ptn_linear_model import PtnLinearModel +from .modules.pytorch_wrapper.ptn_resnet_model import PtnResnetModel +from .modules.pytorch_wrapper.ptn_resnet_model_def import NResnetBlocks +from .modules.pytorch_wrapper.ptn_resnet_model_def import ResnetModel +from .modules.pytorch_wrapper.ptn_resnet_model_def import SingleConvBlock +from .modules.pytorch_wrapper.ptn_resnet_model_def import SingleConvBlockNoActivation +from .modules.pytorch_wrapper.ptn_resnet_model_def import SingleResnetBlock +from .modules.pytorch_wrapper.pto_adam import PtoAdam +from .modules.pytorch_wrapper.ptv_dataset import PtvDataset +from .modules.pytorch_wrapper.ptv_dataset_len import PtvDatasetLen +from .modules.pytorch_wrapper.ptv_dataset_loader import PtvDatasetLoader +from .modules.pytorch_wrapper.ptv_image_folder_dataset import PtvImageFolderDataset +from .modules.pytorch_wrapper.ptv_transforms_resize import PtvTransformsResize +from .modules.pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor +T = TypeVar("T") + + +""" +NODE_CLASS_MAPPINGS (Dict[str, Type[T]]): + A dictionary mapping node names to their corresponding class implementations. +""" + +NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { + "NResnetBlocks": NResnetBlocks, + "PtAbs": PtAbs, + "PtAcos": PtAcos, + "PtAdd": PtAdd, + "PtArange": PtArange, + "PtArgmax": PtArgmax, + "PtArgmin": PtArgmin, + "PtAsin": PtAsin, + "PtAtan": PtAtan, + "PtBitwiseAnd": PtBitwiseAnd, + "PtBitwiseLeftShift": PtBitwiseLeftShift, + "PtBitwiseNot": PtBitwiseNot, + "PtBitwiseOr": PtBitwiseOr, + "PtBitwiseRightShift": PtBitwiseRightShift, + "PtBitwiseXor": PtBitwiseXor, + "PtBmm": PtBmm, + "PtBoolCreate": PtBoolCreate, + "PtCos": PtCos, + "PtCosh": PtCosh, + "PtCrop": PtCrop, + "PtDataLoader": PtDataLoader, + "PtDiv": PtDiv, + "PtEinsum": PtEinsum, + "PtEq": PtEq, + "PtEvaluateClassificationModel": PtEvaluateClassificationModel, + "PtExp": PtExp, + "PtFlatten": PtFlatten, + "PtFloatCreate": PtFloatCreate, + "PtFloorDiv": PtFloorDiv, + "PtFromImage": PtFromImage, + "PtFromImageTranspose": PtFromImageTranspose, + "PtFromLatent": PtFromLatent, + "PtFromNumpy": PtFromNumpy, + "PtFull": PtFull, + "PtGather": PtGather, + "PtGe": PtGe, + "PtGt": PtGt, + "PtIndexSelect": PtIndexSelect, + "PtIntCreate": PtIntCreate, + "PtInterpolateByScaleFactor": PtInterpolateByScaleFactor, + "PtInterpolateToSize": PtInterpolateToSize, + "PtLe": PtLe, + "PtLinspace": PtLinspace, + "PtLoadModel": PtLoadModel, + "PtLog": PtLog, + "PtLogicalAnd": PtLogicalAnd, + "PtLogicalNot": PtLogicalNot, + "PtLogicalOr": PtLogicalOr, + "PtLogicalXor": PtLogicalXor, + "PtLt": PtLt, + "PtMaskedSelect": PtMaskedSelect, + "PtMatMul": PtMatMul, + "PtMax": PtMax, + "PtMean": PtMean, + "PtMedian": PtMedian, + "PtMin": PtMin, + "PtMm": PtMm, + "PtMul": PtMul, + "PtNe": PtNe, + "PtNeg": PtNeg, + "PtOnes": PtOnes, + "PtPad": PtPad, + "PtPermute": PtPermute, + "PtPow": PtPow, + "PtPredictClassificationModel": PtPredictClassificationModel, + "PtProd": PtProd, + "PtRand": PtRand, + "PtRandInt": PtRandInt, + "PtRandn": PtRandn, + "PtRemainder": PtRemainder, + "PtReshape": PtReshape, + "PtSaveModel": PtSaveModel, + "PtScatter": PtScatter, + "PtShowSize": PtShowSize, + "PtShowText": PtShowText, + "PtSin": PtSin, + "PtSinh": PtSinh, + "PtSize": PtSize, + "PtSizeCreate": PtSizeCreate, + "PtSizeToNumpy": PtSizeToNumpy, + "PtSizeToString": PtSizeToString, + "PtSqrt": PtSqrt, + "PtSqueeze": PtSqueeze, + "PtStd": PtStd, + "PtSub": PtSub, + "PtSum": PtSum, + "PtTan": PtTan, + "PtTanh": PtTanh, + "PtToImage": PtToImage, + "PtToImageTranspose": PtToImageTranspose, + "PtToLatent": PtToLatent, + "PtToNumpy": PtToNumpy, + "PtToRgbTensors": PtToRgbTensors, + "PtTrainClassificationModel": PtTrainClassificationModel, + "PtUnsqueeze": PtUnsqueeze, + "PtVar": PtVar, + "PtView": PtView, + "PtWhere": PtWhere, + "PtZeros": PtZeros, + "PtnConvModel": PtnConvModel, + "PtnLinearModel": PtnLinearModel, + "PtnResnetModel": PtnResnetModel, + "PtoAdam": PtoAdam, + "PtvDataset": PtvDataset, + "PtvDatasetLen": PtvDatasetLen, + "PtvDatasetLoader": PtvDatasetLoader, + "PtvImageFolderDataset": PtvImageFolderDataset, + "PtvTransformsResize": PtvTransformsResize, + "PtvTransformsToTensor": PtvTransformsToTensor, + "ResnetModel": ResnetModel, + "SingleConvBlock": SingleConvBlock, + "SingleConvBlockNoActivation": SingleConvBlockNoActivation, + "SingleResnetBlock": SingleResnetBlock, +} + + +""" +NODE_DISPLAY_NAME_MAPPINGS (Dict[str, str]): + A dictionary mapping node names to user-friendly display names. +""" + +NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { + "NResnetBlocks": "N Resnet Blocks", + "PtAbs": "Pt Abs", + "PtAcos": "Pt Acos", + "PtAdd": "Pt Add", + "PtArange": "Pt Arange", + "PtArgmax": "Pt Argmax", + "PtArgmin": "Pt Argmin", + "PtAsin": "Pt Asin", + "PtAtan": "Pt Atan", + "PtBitwiseAnd": "Pt Bitwise And", + "PtBitwiseLeftShift": "Pt Bitwise Left Shift", + "PtBitwiseNot": "Pt Bitwise Not", + "PtBitwiseOr": "Pt Bitwise Or", + "PtBitwiseRightShift": "Pt Bitwise Right Shift", + "PtBitwiseXor": "Pt Bitwise Xor", + "PtBmm": "Pt Bmm", + "PtBoolCreate": "Pt Bool Create", + "PtCos": "Pt Cos", + "PtCosh": "Pt Cosh", + "PtCrop": "Pt Crop", + "PtDataLoader": "Pt Data Loader", + "PtDiv": "Pt Div", + "PtEinsum": "Pt Einsum", + "PtEq": "Pt Eq", + "PtEvaluateClassificationModel": "Pt Evaluate Classification Model", + "PtExp": "Pt Exp", + "PtFlatten": "Pt Flatten", + "PtFloatCreate": "Pt Float Create", + "PtFloorDiv": "Pt Floor Div", + "PtFromImage": "Pt From Image", + "PtFromImageTranspose": "Pt From Image Transpose", + "PtFromLatent": "Pt From Latent", + "PtFromNumpy": "Pt From Numpy", + "PtFull": "Pt Full", + "PtGather": "Pt Gather", + "PtGe": "Pt Ge", + "PtGt": "Pt Gt", + "PtIndexSelect": "Pt Index Select", + "PtIntCreate": "Pt Int Create", + "PtInterpolateByScaleFactor": "Pt Interpolate By Scale Factor", + "PtInterpolateToSize": "Pt Interpolate To Size", + "PtLe": "Pt Le", + "PtLinspace": "Pt Linspace", + "PtLoadModel": "Pt Load Model", + "PtLog": "Pt Log", + "PtLogicalAnd": "Pt Logical And", + "PtLogicalNot": "Pt Logical Not", + "PtLogicalOr": "Pt Logical Or", + "PtLogicalXor": "Pt Logical Xor", + "PtLt": "Pt Lt", + "PtMaskedSelect": "Pt Masked Select", + "PtMatMul": "Pt Mat Mul", + "PtMax": "Pt Max", + "PtMean": "Pt Mean", + "PtMedian": "Pt Median", + "PtMin": "Pt Min", + "PtMm": "Pt Mm", + "PtMul": "Pt Mul", + "PtNe": "Pt Ne", + "PtNeg": "Pt Neg", + "PtOnes": "Pt Ones", + "PtPad": "Pt Pad", + "PtPermute": "Pt Permute", + "PtPow": "Pt Pow", + "PtPredictClassificationModel": "Pt Predict Classification Model", + "PtProd": "Pt Prod", + "PtRand": "Pt Rand", + "PtRandInt": "Pt Rand Int", + "PtRandn": "Pt Randn", + "PtRemainder": "Pt Remainder", + "PtReshape": "Pt Reshape", + "PtSaveModel": "Pt Save Model", + "PtScatter": "Pt Scatter", + "PtShowSize": "Pt Show Size", + "PtShowText": "Pt Show Text", + "PtSin": "Pt Sin", + "PtSinh": "Pt Sinh", + "PtSize": "Pt Size", + "PtSizeCreate": "Pt Size Create", + "PtSizeToNumpy": "Pt Size To Numpy", + "PtSizeToString": "Pt Size To String", + "PtSqrt": "Pt Sqrt", + "PtSqueeze": "Pt Squeeze", + "PtStd": "Pt Std", + "PtSub": "Pt Sub", + "PtSum": "Pt Sum", + "PtTan": "Pt Tan", + "PtTanh": "Pt Tanh", + "PtToImage": "Pt To Image", + "PtToImageTranspose": "Pt To Image Transpose", + "PtToLatent": "Pt To Latent", + "PtToNumpy": "Pt To Numpy", + "PtToRgbTensors": "Pt To Rgb Tensors", + "PtTrainClassificationModel": "Pt Train Classification Model", + "PtUnsqueeze": "Pt Unsqueeze", + "PtVar": "Pt Var", + "PtView": "Pt View", + "PtWhere": "Pt Where", + "PtZeros": "Pt Zeros", + "PtnConvModel": "Ptn Conv Model", + "PtnLinearModel": "Ptn Linear Model", + "PtnResnetModel": "Ptn Resnet Model", + "PtoAdam": "Pto Adam", + "PtvDataset": "Ptv Dataset", + "PtvDatasetLen": "Ptv Dataset Len", + "PtvDatasetLoader": "Ptv Dataset Loader", + "PtvImageFolderDataset": "Ptv Image Folder Dataset", + "PtvTransformsResize": "Ptv Transforms Resize", + "PtvTransformsToTensor": "Ptv Transforms To Tensor", + "ResnetModel": "Resnet Model", + "SingleConvBlock": "Single Conv Block", + "SingleConvBlockNoActivation": "Single Conv Block No Activation", + "SingleResnetBlock": "Single Resnet Block", +} + + +""" +Below two lines were taken from: +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/__init__.py +See credit/credit.md for the full license. +""" + +WEB_DIRECTORY = "./web" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/credit/credit.md b/credit/credit.md new file mode 100644 index 0000000..8b416b7 --- /dev/null +++ b/credit/credit.md @@ -0,0 +1,30 @@ +# Third-party license + +## pythongosssss/ComfyUI-Custom-Scripts +This software is licensed under the following license: +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/LICENSE + +Full text: +``` +MIT License + +Copyright (c) 2023 pythongosssss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/dev/scripts/generate_node_breakdown_csv.py b/dev/scripts/generate_node_breakdown_csv.py new file mode 100644 index 0000000..2e32c55 --- /dev/null +++ b/dev/scripts/generate_node_breakdown_csv.py @@ -0,0 +1,63 @@ +import re +from typing import Dict +import pandas as pd + +def increment_count(d: Dict[str, int], category: str) -> None: + """Increment the count of a category in the dictionary. + + Args: + d (Dict[str, int]): Dictionary storing category counts. + category (str): The category to increment. + """ + d[category] = d.get(category, 0) + 1 + +def process_import_lines(file_path: str) -> Dict[str, int]: + """Extract and categorize import lines from a Python file. + + Args: + file_path (str): Path to the Python file. + + Returns: + Dict[str, int]: Dictionary containing category counts. + """ + category_counts = {} + + with open(file_path, "r") as f: + for line in f: + if "import" in line: + module_name = re.sub(r"^.*import ", "", line).strip() + if module_name.startswith("SNS"): + increment_count(category_counts, "Seaborn") + elif module_name.startswith("MPL"): + increment_count(category_counts, "Matplotlib") + elif module_name.startswith("Pt"): + increment_count(category_counts, "PyTorch") + elif module_name.startswith("Pandas"): + increment_count(category_counts, "Pandas") + elif module_name.startswith(("CDA", "Python")): + increment_count(category_counts, "Misc") + elif module_name.startswith("Numpy"): + increment_count(category_counts, "NumPy") + else: + increment_count(category_counts, "Misc") + + return category_counts + +def save_category_counts_to_csv(category_counts: Dict[str, int], output_file: str) -> None: + """Save category counts as a sorted CSV file. + + Args: + category_counts (Dict[str, int]): Dictionary containing category counts. + output_file (str): Path to the output CSV file. + """ + df = pd.DataFrame(list(category_counts.items()), columns=["Category", "Count"]) + df = df.sort_values(by=["Count"], ascending=False) + df.to_csv(output_file, index=False) + +def main() -> None: + """Main function to process the import categories and save results.""" + category_counts = process_import_lines("__init__.py") + save_category_counts_to_csv(category_counts, "node_count_breakdown.csv") + +if __name__ == "__main__": + main() diff --git a/dev/scripts/init_py_generate.py b/dev/scripts/init_py_generate.py new file mode 100644 index 0000000..2d22740 --- /dev/null +++ b/dev/scripts/init_py_generate.py @@ -0,0 +1,174 @@ +""" +Generate top level __init__.py. + +Run this at the project root if you add a new .py file. + +To skip the python file from included, put +``` +pragma: skip_doc +``` +in the docstring for the class. This pragma is used for both node reference as well as __init__.py. +``` +""" +import os +import ast +import pathlib +from typing import TypeVar + +T = TypeVar("T") +MODULES_DIR = "modules" + + +def extract_classes_from_file(file_path): + with open(file_path, "r", encoding="utf-8") as f: + tree = ast.parse(f.read(), filename=file_path) + + classes = [] + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + docstring = ast.get_docstring(node) + if docstring and "pragma: skip_doc" in docstring: + continue + classes.append(node.name) + return classes + + +def format_display_name(class_name): + result = [] + first_lower_encountered = False + + i = 0 + while i < len(class_name): + if i > 0 and class_name[i].isupper(): + if first_lower_encountered or (i + 1 < len(class_name) and class_name[i + 1].islower()): + result.append(" ") + elif not first_lower_encountered and (i + 1 < len(class_name) and class_name[i + 1].isupper()): + if i + 2 < len(class_name) and class_name[i + 2].islower(): + result.append(" ") + + if class_name[i].islower(): + first_lower_encountered = True + + # Special case for JSON and CSV + if class_name[i:i+4] == "JSON": + if result and result[-1] != " ": + result.append(" ") + result.append("JSON") + if i + 4 < len(class_name) and class_name[i + 4].isupper(): + result.append(" ") + i += 3 # Skip processed letters + elif class_name[i:i+3] == "CSV": + if result and result[-1] != " ": + result.append(" ") + result.append("CSV") + if i + 3 < len(class_name) and class_name[i + 3].isupper(): + result.append(" ") + i += 2 # Skip processed letters + else: + result.append(class_name[i]) + i += 1 + + # Ensure acronyms at the start are not incorrectly split (like "CD A" instead of "CDA") + final_result = "".join(result) + + # TODO: Reduce hack below + replace_pairs = [ + ("CD A", "CDA"), + ("H T M L", "HTML"), + ("MP L", "MPL"), + ("SN S", "SNS"), + (" N A", " NA"), + ("Data Frame", "DataFrame"), + ("Multi Index", "MultiIndex"), + ("MPL Bar", "MPL Bar Chart"), + ("MPL Line", "MPL Line Plot"), + ("MPL Scatter", "MPL Scatter Plot"), + ("SNS Bar", "SNS Bar Chart"), + ("SNS Line", "SNS Line Plot"), + ("SNS Scatter", "SNS Scatter Plot") + ] + for p in replace_pairs: + final_result = final_result.replace(p[0], p[1]) + + return " ".join(final_result.split()) # Remove unintended extra spaces + + +def discover_modules(): + module_entries = [] + for py_file in pathlib.Path(MODULES_DIR).rglob("*.py"): + relative_path = py_file.relative_to(MODULES_DIR).with_suffix("") + module_import_path = ".modules." + str(relative_path).replace(os.sep, ".") + class_names = extract_classes_from_file(py_file) + + for class_name in class_names: + display_name = format_display_name(class_name) + module_entries.append((module_import_path, class_name, display_name)) + + return module_entries + + +def get_class_mappings_doc(): + return """ +\"\"\" +NODE_CLASS_MAPPINGS (Dict[str, Type[T]]): + A dictionary mapping node names to their corresponding class implementations. +\"\"\" +""" + + +def get_display_name_mappings_doc(): + return """ +\"\"\" +NODE_DISPLAY_NAME_MAPPINGS (Dict[str, str]): + A dictionary mapping node names to user-friendly display names. +\"\"\" +""" + + +def get_license_doc(): + return """ +\"\"\" +Below two lines were taken from: +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/__init__.py +See credit/credit.md for the full license. +\"\"\" +""" + + +def generate_init_content(import_statements, node_class_mappings, node_display_name_mappings): + return f"""# This file is auto-generated. Do not edit manually. +from typing import Dict, Type, TypeVar +{os.linesep.join(import_statements)} +T = TypeVar("T") + +{get_class_mappings_doc()} +NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = {{ +{os.linesep.join(node_class_mappings)} +}} + +{get_display_name_mappings_doc()} +NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {{ +{os.linesep.join(node_display_name_mappings)} +}} + +{get_license_doc()} +WEB_DIRECTORY = "./web" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] +""" + + +def generate_init(): + module_entries = discover_modules() + + import_statements = sorted([f"from {entry[0]} import {entry[1]}" for entry in module_entries]) + node_class_mappings = sorted([f' "{entry[1]}": {entry[1]},' for entry in module_entries]) + node_display_name_mappings = sorted([f' "{entry[1]}": "{entry[2]}",' for entry in module_entries]) + + template = generate_init_content(import_statements, node_class_mappings, node_display_name_mappings) + + with open("__init__.py", "w", encoding="utf-8") as f: + f.write(template) + + +if __name__ == "__main__": + generate_init() diff --git a/dev/scripts/reference_generate.py b/dev/scripts/reference_generate.py new file mode 100644 index 0000000..134db7e --- /dev/null +++ b/dev/scripts/reference_generate.py @@ -0,0 +1,159 @@ +""" +Generate node reference documents. + +Run this at the project root if you update a docstring of the node source file. + +To skip the python file from included in the reference, put +``` +pragma: skip_doc +``` +in the docstring for the class. + +To use a custom node name, put the node name on the first line of the docstring for the class with ":". +e.g. +``` + CDA JSON Create: + Creates a serialized JSON object using values entered in the text field. +``` +""" +import os +import re +import ast +from collections import defaultdict +from typing import Dict, List + +# Mapping for data type conversion +data_type_mapping = { + "DATAFRAME": "DataFrame", + "PDSERIES": "Series", + "PDINDEX": "Index" +} + +def convert_data_type(data_type: str) -> str: + """Convert type to the required format.""" + if data_type in data_type_mapping: + return data_type_mapping[data_type] + return data_type.lower().capitalize() + +def extract_class_info(class_def: ast.ClassDef) -> Dict[str, str]: + """Extract class name, title, category, and docstring.""" + class_name = class_def.name + docstring = ast.get_docstring(class_def) or "" + + if "pragma: skip_doc" in docstring: + return None + + lines = docstring.split("\n") + + title = None + category = "Miscellaneous" + + if len(lines) > 1 and lines[0].strip().endswith(":") and lines[0].strip()[0].isupper(): + title = lines.pop(0).strip().rstrip(":") + + if not title: + title = "# " + " ".join(re.findall(r'[A-Z][a-z]*', class_name)) + else: + title = f"# {title}" + + description_lines = [] + for line in lines: + if line.strip().startswith("category:"): + category = line.split(":", 1)[-1].strip() + else: + description_lines.append(line) + + description = "\n".join(description_lines).strip() + + return {"title": title, "description": description, "class_name": class_name, "first_sentence": description_lines[0] if description_lines else "", "category": category} + +def extract_input_types(class_def: ast.ClassDef) -> str: + """Extract INPUT_TYPES definition.""" + input_table = "" + for node in class_def.body: + if isinstance(node, ast.FunctionDef) and node.name == "INPUT_TYPES": + for stmt in ast.walk(node): + if isinstance(stmt, ast.Dict): + for key, value in zip(stmt.keys, stmt.values): + if isinstance(key, ast.Constant) and isinstance(value, ast.Tuple) and len(value.elts) > 0: + param_name = key.value + data_type = value.elts[0].value if isinstance(value.elts[0], ast.Constant) else "" + input_table += f"| {param_name} | {convert_data_type(data_type)} |\n" + return "## Input\n| Name | Data type |\n|---|---|\n" + input_table if input_table else "" + +def extract_return_types(class_def: ast.ClassDef) -> str: + """Extract RETURN_TYPES value.""" + for node in class_def.body: + # Look for assignment or annotated assignment (type hint) statement. + # Node target is the RHS. + if ( + isinstance(node, ast.Assign) and + any(target.id == "RETURN_TYPES" for target in node.targets if isinstance(target, ast.Name)) + ) or ( + isinstance(node, ast.AnnAssign) and + isinstance(node.target, ast.Name) and + node.target.id == "RETURN_TYPES" + ): + if isinstance(node.value, ast.Tuple): + return_types = [convert_data_type(elt.value) for elt in node.value.elts if isinstance(elt, ast.Constant)] + return "## Output\n| Data type |\n|---|\n" + "".join(f"| {rtype} |\n" for rtype in return_types) + return "" + +def process_python_file(file_path: str, output_dir: str, node_list: Dict[str, List[str]]): + """Process a single Python file and generate corresponding Markdown.""" + with open(file_path, "r", encoding="utf-8") as f: + tree = ast.parse(f.read(), filename=file_path) + + for node in tree.body: + if isinstance(node, ast.ClassDef): + class_info = extract_class_info(node) + if not class_info: + return + + input_section = extract_input_types(node) + output_section = extract_return_types(node) + category = class_info["category"] + + file_name = os.path.splitext(os.path.basename(file_path))[0] + ".md" + output_path = os.path.join(output_dir, file_name) + + with open(output_path, "w", encoding="utf-8") as md_file: + md_file.write(f"{class_info['title']}\n{class_info['description']}\n\n") + if input_section: + md_file.write(input_section + "\n") + if output_section: + md_file.write(output_section + "\n") + md_file.write("
\n") + md_file.write(f"Category: {category}\n\n") + md_file.write(f"ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.\n") + + node_list[category].append((class_info['title'][2:], file_name, class_info['first_sentence'])) + +def generate_node_list(output_dir: str, node_list: Dict[str, List[str]]): + """Generate node_reference.md file.""" + node_list_path = os.path.join(output_dir, "node_reference.md") + with open(node_list_path, "w", encoding="utf-8") as md_file: + md_file.write("# Node Reference\n") + for category in sorted(node_list.keys()): + md_file.write(f"## {category}\n| Node | Description |\n| --- | --- |\n") + for title, file_name, description in sorted(node_list[category]): + md_file.write(f"| [{title}]({file_name}) | {description} |\n") + +def main(): + """Main function to process all Python files under modules/**.py.""" + source_dir = "modules" + output_dir = "docs/reference" + os.makedirs(output_dir, exist_ok=True) + + node_list = defaultdict(list) + + for root, _, files in os.walk(source_dir): + for file in files: + if file.endswith(".py"): + file_path = os.path.join(root, file) + process_python_file(file_path, output_dir, node_list) + + generate_node_list(output_dir, node_list) + +if __name__ == "__main__": + main() diff --git a/dev/scripts/run_tests.sh b/dev/scripts/run_tests.sh new file mode 100755 index 0000000..9c295c8 --- /dev/null +++ b/dev/scripts/run_tests.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Run this command on project root +# ``` +# dev/scripts/run_tests.sh +# ```` +# If you add a new test file, make sure that the directory contains an empty +# __init__.py. Otherwise, unittest won't be able to find the directory and +# the tests in the directory will be skipped. +export PYTHONPATH=modules:$PYTHONPATH +echo $PYTHONPATH +RUN_SKIPPED_TESTS=0 python -m unittest discover -s tests -p "*_test.py" -t . + diff --git a/docs/dog_cat_classification_model_training.md b/docs/dog_cat_classification_model_training.md new file mode 100644 index 0000000..68f3c13 --- /dev/null +++ b/docs/dog_cat_classification_model_training.md @@ -0,0 +1,137 @@ +# Dog cat classification model training tutorial +This tutorial covers how to use **ComfyUI Data Analysis** to train an image classification model with your own image dataset. +We'll use a **dog and cat image dataset** created specifically for this tutorial, which is available publicly on [my GitHub repo](https://github.com/HowToSD/dog_and_cat_dataset). + +Once you complete the tutorial, you can experiment with your own images. + +**Disclaimer:** The model architecture is a traditional convolutional neural network (CNN) and is **not state-of-the-art**. This choice keeps the model small, enabling faster training and easier explanation. + +--- + +## Overview of Steps + +1. Downloading the dog and cat dataset from the GitHub repo +2. Edit the training workflow +3. Run the training workflow +4. Edit the evaluation workflow +5. Run the evaluation workflow + +--- + +## 1. Download the Dog & Cat Dataset + +Visit [the GitHub repo](https://github.com/HowToSD/dog_and_cat_dataset) and go to the **Releases** page. +Click **"Source code (zip)"** to download the zip that contains all the images. +Extract the ZIP file using an unarchiver. + +The dataset follows this directory structure: + +``` +dog_and_cat + train + cats + dogs + + val + cats + dogs +``` + + +Take note of the **full paths** to the `train` and `val` directories, as you'll need them in the next step. + +--- + +## 2. Editing the Training Workflow + +Drag and drop the below file into ComfyUI: +``` +examples/workflow/dog_cat_classification_train_model.json +``` +Some nodes will be marked red, indicating they require edits. +The only required changes are the **train** and **val** dataset paths. +Replace them with the paths noted earlier. + +--- + +## 3. Running the Training Workflow + +Click **Queue** to start training. + +Monitor the console for errors. If training runs smoothly, you should see output like this: + +``` +Epoch (train) 1/20, Loss: 0.6865 +Epoch (val) 1/20, Loss: 0.6801 +Epoch (train) 2/20, Loss: 0.6216 +Epoch (val) 2/20, Loss: 0.5710 +Epoch (train) 3/20, Loss: 0.4998 +Epoch (val) 3/20, Loss: 0.4716 +Epoch (train) 4/20, Loss: 0.3827 +Epoch (val) 4/20, Loss: 0.4045 +Epoch (train) 5/20, Loss: 0.2897 +Epoch (val) 5/20, Loss: 0.3297 +Epoch (train) 6/20, Loss: 0.2243 +Epoch (val) 6/20, Loss: 0.2925 +Epoch (train) 7/20, Loss: 0.1603 +Epoch (val) 7/20, Loss: 0.2916 +Epoch (train) 8/20, Loss: 0.1102 +Epoch (val) 8/20, Loss: 0.3216 +Epoch (train) 9/20, Loss: 0.0916 +Epoch (val) 9/20, Loss: 0.2706 +Epoch (train) 10/20, Loss: 0.0522 +Epoch (val) 10/20, Loss: 0.2220 +Epoch (train) 11/20, Loss: 0.0481 +Epoch (val) 11/20, Loss: 0.2820 +Epoch (train) 12/20, Loss: 0.0228 +Epoch (val) 12/20, Loss: 0.2899 +Epoch (train) 13/20, Loss: 0.0130 +Epoch (val) 13/20, Loss: 0.3056 +Epoch (train) 14/20, Loss: 0.0137 +Epoch (val) 14/20, Loss: 0.3070 +Epoch (train) 15/20, Loss: 0.0200 +Epoch (val) 15/20, Loss: 0.2714 +Epoch (train) 16/20, Loss: 0.0313 +Epoch (val) 16/20, Loss: 0.3108 +Epoch (train) 17/20, Loss: 0.0125 +Epoch (val) 17/20, Loss: 0.3388 +Epoch (train) 18/20, Loss: 0.0174 +Epoch (val) 18/20, Loss: 0.2989 +Epoch (train) 19/20, Loss: 0.0017 +Epoch (val) 19/20, Loss: 0.3165 +Epoch (train) 20/20, Loss: 0.0009 +Epoch (val) 20/20, Loss: 0.3582 +Prompt executed in 253.64 seconds +``` + +If you encounter an error: +- Ensure dataset paths are correct. +- Reduce **batch size** if your GPU runs out of memory (OOM error). + +Once training completes, a **line chart** displaying training progress will appear at the bottom right of the canvas. You don’t need to interpret it initially. + +--- + +## 4. Editing the Evaluation Workflow +Now, let's verify if the model was trained correctly. In this step, we'll feed the validation data into the trained model and compute metrics to assess its performance. + +Drag and drop the below file into ComfyUI: +``` +examples/workflow/dog_cat_classification_eval_model.json +``` + +into **ComfyUI**. + +Update the **val dataset path**. + +--- + +## 5. Running the Evaluation Workflow + +Click **Queue**. The evaluation process runs much faster than training. + +Check the **Accuracy** node—it should display a value around **0.9**, confirming successful model training. + +Congratulations! You’ve successfully trained a machine learning model from scratch without writing a single line of code. + +Now, you can use your own images to train the model in the same way. \ No newline at end of file diff --git a/docs/model_training.md b/docs/model_training.md new file mode 100644 index 0000000..dbfc555 --- /dev/null +++ b/docs/model_training.md @@ -0,0 +1,49 @@ +# Model Training + +Experimental PyTorch model training has been added. +No coding is required. Training is as simple as a single click! +Currently, only classification is supported. + +The system is designed to be modular. For example, to switch from a linear model to a convolutional model, you only need to replace the model node (see screenshot below). + +You can choose to: +* Use your own images +* Use a public dataset + +## Using Your Own Images + +Check out [the dog and cat classification tutorial](dog_cat_classification_model_training.md). + +## Public Datasets + +Currently, only Fashion MNIST has been tested. Other public datasets have not been verified yet, though they may work. +If you try one and it doesn't work, please create an issue so I can investigate adding support. + +To use the Fashion MNIST dataset, simply drag and drop the workflow files below into ComfyUI. The model will be downloaded automatically. + +Linear model +* Training: examples/fashion_mnist_train.json +* Evaluation: examples/fashion_mnist_eval.json + +Convolutional model +* Training: examples/fashion_mnist_train_conv.json +* Evaluation: examples/fashion_mnist_eval_conv.json + +These training workflows train a FashionMNIST model from scratch effortlessly. + +### Training workflow +**Linear model** +![Train](images/fashion_mnist_train.png) + +**Conv model** +![Train](images/conv_train.png) + +*Close up of the conv node* +![Train](images/conv_train2.png) + +### Eval workflow +**Linear model** +![Eval](images/fashion_mnist_eval.png) + +**Conv model** +![Eval](images/conv_eval.png) diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md new file mode 100644 index 0000000..54898f6 --- /dev/null +++ b/docs/reference/node_reference.md @@ -0,0 +1,165 @@ +# Node Reference +## Display data +| Node | Description | +| --- | --- | +| [Pt Show Size](pt_show_size.md) | Displays PyTorch Size object as a string. | +| [Pt Show Text](pt_show_text.md) | Displays PyTorch tensor as a string. Note that the tensor is partially printed out when | +## IO +| Node | Description | +| --- | --- | +| [Pt Save Model](pt_save_model.md) | A wrapper class for saving a PyTorch model. | +## Miscellaneous +| Node | Description | +| --- | --- | +| [N Resnet Blocks](ptn_resnet_model_def.md) | | +| [Resnet Model](ptn_resnet_model_def.md) | | +| [Single Conv Block](ptn_resnet_model_def.md) | | +| [Single Conv Block No Activation](ptn_resnet_model_def.md) | | +| [Single Resnet Block](ptn_resnet_model_def.md) | | +## PyTorch wrapper - Arithmetic operations +| Node | Description | +| --- | --- | +| [Pt Add](pt_add.md) | Adds two PyTorch tensors. | +| [Pt Div](pt_div.md) | Divides one PyTorch tensor by another element-wise. | +| [Pt Floor Div](pt_floor_divide.md) | Performs element-wise floor division on two PyTorch tensors. | +| [Pt Mul](pt_mul.md) | Multiplies two PyTorch tensors element-wise. | +| [Pt Pow](pt_pow.md) | Raises one PyTorch tensor to the power of another element-wise. | +| [Pt Remainder](pt_remainder.md) | Computes the element-wise remainder of division between two PyTorch tensors. | +| [Pt Sub](pt_sub.md) | Subtracts one PyTorch tensor from another. | +## PyTorch wrapper - Bitwise operations +| Node | Description | +| --- | --- | +| [Pt Bitwise And](pt_bitwise_and.md) | Performs a bitwise AND operation on two PyTorch tensors element-wise. | +| [Pt Bitwise Left Shift](pt_bitwise_left_shift.md) | Performs a bitwise left shift operation on two PyTorch tensors element-wise. | +| [Pt Bitwise Not](pt_bitwise_not.md) | Performs a bitwise NOT operation on a PyTorch tensor element-wise. | +| [Pt Bitwise Or](pt_bitwise_or.md) | Performs a bitwise OR operation on two PyTorch tensors element-wise. | +| [Pt Bitwise Right Shift](pt_bitwise_right_shift.md) | Performs a bitwise right shift operation on two PyTorch tensors element-wise. | +| [Pt Bitwise Xor](pt_bitwise_xor.md) | Performs a bitwise XOR operation on two PyTorch tensors element-wise. | +## PyTorch wrapper - Comparison operations +| Node | Description | +| --- | --- | +| [Pt Eq](pt_eq.md) | Tests whether two PyTorch tensors are equal element-wise. | +| [Pt Ge](pt_ge.md) | Tests whether elements in the first PyTorch tensor are greater than or equal to the corresponding elements in the second tensor. | +| [Pt Gt](pt_gt.md) | Tests whether elements in the first PyTorch tensor are greater than the corresponding elements in the second tensor. | +| [Pt Le](pt_le.md) | Tests whether elements in the first PyTorch tensor are less than or equal to the corresponding elements in the second tensor. | +| [Pt Lt](pt_lt.md) | Tests whether elements in the first PyTorch tensor are less than the corresponding elements in the second tensor. | +| [Pt Ne](pt_ne.md) | Tests whether two PyTorch tensors are not equal element-wise. | +## PyTorch wrapper - Image processing +| Node | Description | +| --- | --- | +| [Pt Crop](pt_crop.md) | Crops a PyTorch tensor to the specified size. The input tensor must have a shape of (c, h, w) or (b, c, h, w). | +| [Pt From Image](pt_from_image.md) | Casts an Image tensor as a PyTorch tensor. | +| [Pt From Image Transpose](pt_from_image_transpose.md) | Casts an image tensor to a PyTorch tensor and transposes it from (H, W, C) to (C, H, W). For rank-4 inputs, the batch axis remains unchanged. | +| [Pt Interpolate By Scale Factor](pt_interpolate_by_scale_factor.md) | Resizes a PyTorch tensor using interpolation by scale factor. The input tensor must have a shape of (c, h, w) or (b, c, h, w). | +| [Pt Interpolate To Size](pt_interpolate_to_size.md) | Resizes a PyTorch tensor using interpolation. The input tensor must have a shape of (c, h, w) or (b, c, h, w). | +| [Pt Pad](pt_pad.md) | Pads a PyTorch tensor to the specified size. Padded area will be black. The input tensor must have a shape of (c, h, w) or (b, c, h, w). | +| [Pt To Image](pt_to_image.md) | Casts a PyTorch tensor as an Image tensor. | +| [Pt To Image Transpose](pt_to_image_transpose.md) | Casts a PyTorch tensor as an Image tensor and transposes it from (C, H, W) to (H, W, C). For rank-4 inputs, the batch axis remains unchanged. | +## PyTorch wrapper - Indexing and Slicing Operations +| Node | Description | +| --- | --- | +| [Pt Gather](pt_gather.md) | Generates a tensor based on the index tensor using PyTorch's `gather` function. | +| [Pt Index Select](pt_index_select.md) | Extracts elements from the input tensor along a specified dimension using an index tensor. | +| [Pt Masked Select](pt_masked_select.md) | Extracts elements from the input tensor whose corresponding value in `masked_tens` is `True`. | +| [Pt Scatter](pt_scatter.md) | Generates a new tensor by replacing values at specified positions using an index tensor. | +| [Pt Where](pt_where.md) | Generates a new tensor by selecting values based on a condition tensor. | +## PyTorch wrapper - Logical operations +| Node | Description | +| --- | --- | +| [Pt Logical And](pt_logical_and.md) | Performs a logical AND operation on two PyTorch tensors element-wise. | +| [Pt Logical Not](pt_logical_not.md) | Performs a logical NOT operation on a PyTorch tensor element-wise. | +| [Pt Logical Or](pt_logical_or.md) | Performs a logical OR operation on two PyTorch tensors element-wise. | +| [Pt Logical Xor](pt_logical_xor.md) | Performs a logical XOR operation on two PyTorch tensors element-wise. | +## PyTorch wrapper - Math operations +| Node | Description | +| --- | --- | +| [Pt Abs](pt_abs.md) | Computes the absolute value of each element in a PyTorch tensor. | +| [Pt Acos](pt_acos.md) | Computes the arccosine (inverse cosine) of a PyTorch tensor element-wise. | +| [Pt Asin](pt_asin.md) | Computes the arcsine (inverse sine) of a PyTorch tensor element-wise. | +| [Pt Atan](pt_atan.md) | Computes the arc tangent (inverse tangent) of a PyTorch tensor element-wise. | +| [Pt Cos](pt_cos.md) | Computes the cosine of a PyTorch tensor element-wise. | +| [Pt Cosh](pt_cosh.md) | Computes the hyperbolic cosine of a PyTorch tensor element-wise. | +| [Pt Exp](pt_exp.md) | Performs an exponential operation on a PyTorch tensor element-wise. | +| [Pt Log](pt_log.md) | Computes the natural logarithm (log base e) of a PyTorch tensor element-wise. | +| [Pt Neg](pt_neg.md) | Computes the negation of each element in a PyTorch tensor. | +| [Pt Sin](pt_sin.md) | Computes the sine of a PyTorch tensor element-wise. | +| [Pt Sinh](pt_sinh.md) | Computes the hyperbolic sine of a PyTorch tensor element-wise. | +| [Pt Sqrt](pt_sqrt.md) | Computes the square root of each element in a PyTorch tensor. | +| [Pt Tan](pt_tan.md) | Computes the tangent of a PyTorch tensor element-wise. | +| [Pt Tanh](pt_tanh.md) | Computes the hyperbolic tangent of a PyTorch tensor element-wise. | +## PyTorch wrapper - Matrix operations +| Node | Description | +| --- | --- | +| [Pt Bmm](pt_bmm.md) | Performs batched matrix multiplication on two 3D PyTorch tensors. | +| [Pt Einsum](pt_einsum.md) | Performs Tensor operations specified in the Einstein summation equation. | +| [Pt Mat Mul](pt_matmul.md) | Performs matrix multiplication on two PyTorch tensors. | +| [Pt Mm](pt_mm.md) | Performs 2D matrix multiplication on two PyTorch tensors. | +## PyTorch wrapper - Reduction operation & Summary statistics +| Node | Description | +| --- | --- | +| [Pt Argmax](pt_argmax.md) | Computes the indices of the maximum values of a PyTorch tensor along the specified dimension(s). | +| [Pt Argmin](pt_argmin.md) | Computes the indices of the minimum values of a PyTorch tensor along the specified dimension(s). | +| [Pt Max](pt_max.md) | Computes the maximum values of a PyTorch tensor along the specified dimension(s). | +| [Pt Mean](pt_mean.md) | Computes the mean of a PyTorch tensor along the specified dimension(s). | +| [Pt Median](pt_median.md) | Computes the median of a PyTorch tensor along the specified dimension(s). | +| [Pt Min](pt_min.md) | Computes the minimum values of a PyTorch tensor along the specified dimension(s). | +| [Pt Prod](pt_prod.md) | Computes the product of a PyTorch tensor along the specified dimension(s). | +| [Pt Std](pt_std.md) | Computes the standard deviation of a PyTorch tensor along the specified dimension(s). | +| [Pt Sum](pt_sum.md) | Computes the sum of a PyTorch tensor along the specified dimension(s). | +| [Pt Var](pt_var.md) | Computes the variance of a PyTorch tensor along the specified dimension(s). | +## PyTorch wrapper - Size object support +| Node | Description | +| --- | --- | +| [Pt Size](pt_size.md) | Extracts the PyTorch Size object of a PyTorch tensor using the size() method. | +| [Pt Size Create](pt_size_create.md) | Creates a PyTorch Size using values entered in the text field. | +| [Pt Size To Numpy](pt_size_to_numpy.md) | Converts PyTorch Size object to NumPy ndarray. | +| [Pt Size To String](pt_size_to_string.md) | Converts PyTorch Size object to a Python string. | +## PyTorch wrapper - Tensor creation +| Node | Description | +| --- | --- | +| [Pt Arange](pt_arange.md) | Creates a PyTorch tensor using `torch.arange` with the specified start, end, and step values. | +| [Pt Bool Create](pt_bool_create.md) | Creates a PyTorch tensor of dtype bool from True or False values entered as a list in the text field. | +| [Pt Float Create](pt_float_create.md) | Creates a PyTorch tensor with 32-bit floating point precision | +| [Pt From Latent](pt_from_latent.md) | Casts a latent tensor as a PyTorch tensor. | +| [Pt From Numpy](pt_from_numpy.md) | Converts a NumPy ndarray to a PyTorch tensor while preserving its data type. | +| [Pt Full](pt_full.md) | Creates a PyTorch tensor filled with a specified value using the size entered in the text field. | +| [Pt Int Create](pt_int_create.md) | Creates a PyTorch tensor with 32-bit integer | +| [Pt Linspace](pt_linspace.md) | Creates a PyTorch tensor using `torch.linspace` with the specified start, end, and steps values. | +| [Pt Ones](pt_ones.md) | Creates a PyTorch tensor of ones using the size entered in the text field. | +| [Pt Rand](pt_rand.md) | Creates a PyTorch tensor with values sampled from a uniform distribution | +| [Pt Rand Int](pt_rand_int.md) | Creates a PyTorch tensor filled with random integers within a specified range using the size entered in the text field. | +| [Pt Randn](pt_randn.md) | Creates a PyTorch tensor with values sampled from a standard normal distribution (mean=0, std=1) | +| [Pt Zeros](pt_zeros.md) | Creates a PyTorch tensor of zeros using the size entered in the text field. | +## PyTorch wrapper - Tensor data conversion +| Node | Description | +| --- | --- | +| [Pt To Latent](pt_to_latent.md) | Casts a PyTorch tensor as a latent tensor. | +| [Pt To Numpy](pt_to_numpy.md) | Converts PyTorch tensor to NumPy ndarray. | +| [Pt To Rgb Tensors](pt_to_rgb_tensors.md) | Splits a PyTorch tensor into R, G, and B tensors. | +## PyTorch wrapper - Training +| Node | Description | +| --- | --- | +| [Pt Data Loader](pt_data_loader.md) | Loads data from a dataset node and creates a PyTorch DataLoader. | +| [Pt Evaluate Classification Model](pt_evaluate_classification_model.md) | Performs inference on test data and computes evaluation metrics. | +| [Pt Load Model](pt_load_model.md) | A wrapper class for saving a PyTorch model. | +| [Pt Predict Classification Model](pt_predict_classification_model.md) | Performs inference on input data. | +| [Pt Train Classification Model](pt_train_classification_model.md) | Trains a classification model using a given dataset, optimizer, and number of epochs. | +| [Ptn Conv Model](ptn_conv_model.md) | A convolutional model consisting of multiple convolutional layers. | +| [Ptn Linear Model](ptn_linear_model.md) | A linear model consisting of dense layers. | +| [Ptn Resnet Model](ptn_resnet_model.md) | A Resnet model consisting of multiple Resnet layers. | +| [Pto Adam](pto_adam.md) | Instantiates the Adam optimizer. | +| [Ptv Dataset](ptv_dataset.md) | A Torchvision Dataset class wrapper. | +| [Ptv Dataset Len](ptv_dataset_len.md) | A wrapper class that calls Python len on a dataset. | +| [Ptv Dataset Loader](ptv_dataset_loader.md) | A node to combine the dataset and data loader into a single node. | +| [Ptv Image Folder Dataset](ptv_image_folder_dataset.md) | A Torchvision ImageFolder Dataset class wrapper. | +| [Ptv Transforms Resize](ptv_transforms_resize.md) | Resizes and transforms elements of dataset to PyTorch tensors. | +| [Ptv Transforms To Tensor](ptv_transforms_to_tensor.md) | Transforms elements of dataset to PyTorch tensors. | +## PyTorch wrapper - Transform +| Node | Description | +| --- | --- | +| [Pt Flatten](pt_flatten.md) | Flattens a PyTorch tensor into a 1D tensor. | +| [Pt Permute](pt_permute.md) | Permutes the dimensions of a PyTorch tensor according to the specified order. | +| [Pt Reshape](pt_reshape.md) | Reshapes a PyTorch tensor into a specified shape using `torch.reshape()`. | +| [Pt Squeeze](pt_squeeze.md) | Removes a dimension at the specified position in the input tensor if it is of size 1. | +| [Pt Unsqueeze](pt_unsqueeze.md) | Adds a singleton dimension at the specified position in the input tensor. | +| [Pt View](pt_view.md) | Reshapes a PyTorch tensor into a specified shape using `torch.view()`. | diff --git a/docs/reference/pt_abs.md b/docs/reference/pt_abs.md new file mode 100644 index 0000000..2565442 --- /dev/null +++ b/docs/reference/pt_abs.md @@ -0,0 +1,17 @@ +# Pt Abs +Computes the absolute value of each element in a PyTorch tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_acos.md b/docs/reference/pt_acos.md new file mode 100644 index 0000000..6df37ca --- /dev/null +++ b/docs/reference/pt_acos.md @@ -0,0 +1,17 @@ +# Pt Acos +Computes the arccosine (inverse cosine) of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_add.md b/docs/reference/pt_add.md new file mode 100644 index 0000000..070bfd0 --- /dev/null +++ b/docs/reference/pt_add.md @@ -0,0 +1,18 @@ +# Pt Add +Adds two PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_arange.md b/docs/reference/pt_arange.md new file mode 100644 index 0000000..3a5e325 --- /dev/null +++ b/docs/reference/pt_arange.md @@ -0,0 +1,23 @@ +# Pt Arange +Creates a PyTorch tensor using `torch.arange` with the specified start, end, and step values. + +The values are parsed from string inputs and converted to numeric types. This allows users to specify +tensor range values in text fields. The function also ensures proper dtype selection. + +## Input +| Name | Data type | +|---|---| +| start | String | +| end | String | +| step | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_argmax.md b/docs/reference/pt_argmax.md new file mode 100644 index 0000000..239bffc --- /dev/null +++ b/docs/reference/pt_argmax.md @@ -0,0 +1,24 @@ +# Pt Argmax +Computes the indices of the maximum values of a PyTorch tensor along the specified dimension(s). + +Specify the dimension in the `dim` field using an integer as shown below: +``` +0 +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_argmin.md b/docs/reference/pt_argmin.md new file mode 100644 index 0000000..d08c285 --- /dev/null +++ b/docs/reference/pt_argmin.md @@ -0,0 +1,24 @@ +# Pt Argmin +Computes the indices of the minimum values of a PyTorch tensor along the specified dimension(s). + +Specify the dimension in the `dim` field using an integer as shown below: +``` +0 +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_asin.md b/docs/reference/pt_asin.md new file mode 100644 index 0000000..8700137 --- /dev/null +++ b/docs/reference/pt_asin.md @@ -0,0 +1,17 @@ +# Pt Asin +Computes the arcsine (inverse sine) of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_atan.md b/docs/reference/pt_atan.md new file mode 100644 index 0000000..d745a09 --- /dev/null +++ b/docs/reference/pt_atan.md @@ -0,0 +1,17 @@ +# Pt Atan +Computes the arc tangent (inverse tangent) of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bitwise_and.md b/docs/reference/pt_bitwise_and.md new file mode 100644 index 0000000..3678455 --- /dev/null +++ b/docs/reference/pt_bitwise_and.md @@ -0,0 +1,18 @@ +# Pt Bitwise And +Performs a bitwise AND operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Bitwise operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bitwise_left_shift.md b/docs/reference/pt_bitwise_left_shift.md new file mode 100644 index 0000000..17ef621 --- /dev/null +++ b/docs/reference/pt_bitwise_left_shift.md @@ -0,0 +1,18 @@ +# Pt Bitwise Left Shift +Performs a bitwise left shift operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Bitwise operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bitwise_not.md b/docs/reference/pt_bitwise_not.md new file mode 100644 index 0000000..fd0080f --- /dev/null +++ b/docs/reference/pt_bitwise_not.md @@ -0,0 +1,17 @@ +# Pt Bitwise Not +Performs a bitwise NOT operation on a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Bitwise operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bitwise_or.md b/docs/reference/pt_bitwise_or.md new file mode 100644 index 0000000..2bea79d --- /dev/null +++ b/docs/reference/pt_bitwise_or.md @@ -0,0 +1,18 @@ +# Pt Bitwise Or +Performs a bitwise OR operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Bitwise operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bitwise_right_shift.md b/docs/reference/pt_bitwise_right_shift.md new file mode 100644 index 0000000..b206a8f --- /dev/null +++ b/docs/reference/pt_bitwise_right_shift.md @@ -0,0 +1,18 @@ +# Pt Bitwise Right Shift +Performs a bitwise right shift operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Bitwise operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bitwise_xor.md b/docs/reference/pt_bitwise_xor.md new file mode 100644 index 0000000..4074bb6 --- /dev/null +++ b/docs/reference/pt_bitwise_xor.md @@ -0,0 +1,18 @@ +# Pt Bitwise Xor +Performs a bitwise XOR operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Bitwise operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bmm.md b/docs/reference/pt_bmm.md new file mode 100644 index 0000000..151f974 --- /dev/null +++ b/docs/reference/pt_bmm.md @@ -0,0 +1,18 @@ +# Pt Bmm +Performs batched matrix multiplication on two 3D PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Matrix operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_bool_create.md b/docs/reference/pt_bool_create.md new file mode 100644 index 0000000..e342555 --- /dev/null +++ b/docs/reference/pt_bool_create.md @@ -0,0 +1,22 @@ +# Pt Bool Create +Creates a PyTorch tensor of dtype bool from True or False values entered as a list in the text field. +For example, entering: +``` +[True, False, False] +``` +will create a 1D boolean tensor. + +## Input +| Name | Data type | +|---|---| +| data | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_cos.md b/docs/reference/pt_cos.md new file mode 100644 index 0000000..d180b87 --- /dev/null +++ b/docs/reference/pt_cos.md @@ -0,0 +1,17 @@ +# Pt Cos +Computes the cosine of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_cosh.md b/docs/reference/pt_cosh.md new file mode 100644 index 0000000..9e3b309 --- /dev/null +++ b/docs/reference/pt_cosh.md @@ -0,0 +1,17 @@ +# Pt Cosh +Computes the hyperbolic cosine of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_crop.md b/docs/reference/pt_crop.md new file mode 100644 index 0000000..44ed92b --- /dev/null +++ b/docs/reference/pt_crop.md @@ -0,0 +1,23 @@ +# Pt Crop +Crops a PyTorch tensor to the specified size. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + +In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + +This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| height | Int | +| width | Int | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_data_loader.md b/docs/reference/pt_data_loader.md new file mode 100644 index 0000000..2ae51c8 --- /dev/null +++ b/docs/reference/pt_data_loader.md @@ -0,0 +1,29 @@ +# Pt Data Loader +Loads data from a dataset node and creates a PyTorch DataLoader. + +**dataset** +Connect a PtvDataset node that provides the dataset. + +**batch size** +Specifies the number of samples per batch. + +**shuffle** +If True, shuffles the dataset before each epoch. + +## Input +| Name | Data type | +|---|---| +| dataset | Ptvdataset | +| batch_size | Int | +| shuffle | Boolean | +| parameters | String | + +## Output +| Data type | +|---| +| Ptdataloader | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_div.md b/docs/reference/pt_div.md new file mode 100644 index 0000000..585cdb7 --- /dev/null +++ b/docs/reference/pt_div.md @@ -0,0 +1,18 @@ +# Pt Div +Divides one PyTorch tensor by another element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_einsum.md b/docs/reference/pt_einsum.md new file mode 100644 index 0000000..fa1fcda --- /dev/null +++ b/docs/reference/pt_einsum.md @@ -0,0 +1,26 @@ +# Pt Einsum +Performs Tensor operations specified in the Einstein summation equation. + +Specify Einstein summation notation in the first socket. +Specify two tensors in tens_a, tens_b. If you specify the third tensor, specify in tens_c. +Otherwise, leave the socket unconnected. + +Sublist format is not supported. + +## Input +| Name | Data type | +|---|---| +| equation | String | +| tens_a | Tensor | +| tens_b | Tensor | +| tens_c | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Matrix operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_eq.md b/docs/reference/pt_eq.md new file mode 100644 index 0000000..9228fa7 --- /dev/null +++ b/docs/reference/pt_eq.md @@ -0,0 +1,18 @@ +# Pt Eq +Tests whether two PyTorch tensors are equal element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Comparison operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_evaluate_classification_model.md b/docs/reference/pt_evaluate_classification_model.md new file mode 100644 index 0000000..c78bce9 --- /dev/null +++ b/docs/reference/pt_evaluate_classification_model.md @@ -0,0 +1,23 @@ +# Pt Evaluate Classification Model +Performs inference on test data and computes evaluation metrics. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| data_loader | Ptdataloader | +| use_gpu | Boolean | + +## Output +| Data type | +|---| +| Float | +| Float | +| Float | +| Float | +| Int | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_exp.md b/docs/reference/pt_exp.md new file mode 100644 index 0000000..8c4ffe0 --- /dev/null +++ b/docs/reference/pt_exp.md @@ -0,0 +1,17 @@ +# Pt Exp +Performs an exponential operation on a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_flatten.md b/docs/reference/pt_flatten.md new file mode 100644 index 0000000..9c32483 --- /dev/null +++ b/docs/reference/pt_flatten.md @@ -0,0 +1,17 @@ +# Pt Flatten +Flattens a PyTorch tensor into a 1D tensor. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Transform + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_float_create.md b/docs/reference/pt_float_create.md new file mode 100644 index 0000000..d84a389 --- /dev/null +++ b/docs/reference/pt_float_create.md @@ -0,0 +1,18 @@ +# Pt Float Create +Creates a PyTorch tensor with 32-bit floating point precision +using values entered in the text field. + +## Input +| Name | Data type | +|---|---| +| data | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_floor_divide.md b/docs/reference/pt_floor_divide.md new file mode 100644 index 0000000..8f52b4f --- /dev/null +++ b/docs/reference/pt_floor_divide.md @@ -0,0 +1,18 @@ +# Pt Floor Div +Performs element-wise floor division on two PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_from_image.md b/docs/reference/pt_from_image.md new file mode 100644 index 0000000..75c6409 --- /dev/null +++ b/docs/reference/pt_from_image.md @@ -0,0 +1,17 @@ +# Pt From Image +Casts an Image tensor as a PyTorch tensor. + +## Input +| Name | Data type | +|---|---| +| image | Image | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_from_image_transpose.md b/docs/reference/pt_from_image_transpose.md new file mode 100644 index 0000000..767e5b9 --- /dev/null +++ b/docs/reference/pt_from_image_transpose.md @@ -0,0 +1,17 @@ +# Pt From Image Transpose +Casts an image tensor to a PyTorch tensor and transposes it from (H, W, C) to (C, H, W). For rank-4 inputs, the batch axis remains unchanged. + +## Input +| Name | Data type | +|---|---| +| image | Image | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_from_latent.md b/docs/reference/pt_from_latent.md new file mode 100644 index 0000000..5ecb5b3 --- /dev/null +++ b/docs/reference/pt_from_latent.md @@ -0,0 +1,17 @@ +# Pt From Latent +Casts a latent tensor as a PyTorch tensor. + +## Input +| Name | Data type | +|---|---| +| latent | Latent | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_from_numpy.md b/docs/reference/pt_from_numpy.md new file mode 100644 index 0000000..105284d --- /dev/null +++ b/docs/reference/pt_from_numpy.md @@ -0,0 +1,17 @@ +# Pt From Numpy +Converts a NumPy ndarray to a PyTorch tensor while preserving its data type. + +## Input +| Name | Data type | +|---|---| +| array | Ndarray | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_full.md b/docs/reference/pt_full.md new file mode 100644 index 0000000..df0a41b --- /dev/null +++ b/docs/reference/pt_full.md @@ -0,0 +1,21 @@ +# Pt Full +Creates a PyTorch tensor filled with a specified value using the size entered in the text field. +For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32 filled with 7, enter: +[2,3,96,32] for size and 7 for value. + +## Input +| Name | Data type | +|---|---| +| value | String | +| size | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_gather.md b/docs/reference/pt_gather.md new file mode 100644 index 0000000..9da9410 --- /dev/null +++ b/docs/reference/pt_gather.md @@ -0,0 +1,59 @@ +# Pt Gather +Generates a tensor based on the index tensor using PyTorch's `gather` function. + +Specify the `dim` field using an integer to indicate the axis along which to gather elements. +Each input tensor element is selected based on the index tensor. + +For example, consider the following input tensor: +``` +[ [10, 20, 30, 40, 50], + [100, 200, 300, 400, 500]] +``` + +and the corresponding index tensor: + +``` +[ [0, 4, 3, 2, 0], + [4, 3, 0, 0, 0]] +``` + +If `dim = 0`, the function scans row-wise, treating each index value as a row index. +If `dim = 1`, it scans column-wise, interpreting each index value as a column index. + +Let's analyze the case where `dim = 1`: + +* Start at row 0, column 0. +* The index at this position is `index[0, 0] = 0`. +* PyTorch selects the value at position 0 from the array `[10, 20, 30, 40, 50]`, which is `10`. +* The result at this stage is: + ``` + [[10, ?, ?, ?, ?] + [?, ?, ?, ?, ?]] + ``` + +* Moving to column 1: `index[0, 1] = 4`. +* The value at index 4 in `[10, 20, 30, 40, 50]` is `50`. +* The updated result is: + ``` + [[10, 50, ?, ?, ?] + [?, ?, ?, ?, ?]] + ``` + +* Repeating this process for all elements constructs the final gathered tensor. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | Int | +| index | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Indexing and Slicing Operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_ge.md b/docs/reference/pt_ge.md new file mode 100644 index 0000000..bd7b39c --- /dev/null +++ b/docs/reference/pt_ge.md @@ -0,0 +1,18 @@ +# Pt Ge +Tests whether elements in the first PyTorch tensor are greater than or equal to the corresponding elements in the second tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Comparison operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_gt.md b/docs/reference/pt_gt.md new file mode 100644 index 0000000..5401090 --- /dev/null +++ b/docs/reference/pt_gt.md @@ -0,0 +1,18 @@ +# Pt Gt +Tests whether elements in the first PyTorch tensor are greater than the corresponding elements in the second tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Comparison operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_index_select.md b/docs/reference/pt_index_select.md new file mode 100644 index 0000000..09e6778 --- /dev/null +++ b/docs/reference/pt_index_select.md @@ -0,0 +1,41 @@ +# Pt Index Select +Extracts elements from the input tensor along a specified dimension using an index tensor. + +`dim` specifies the dimension along which the selection occurs. +* For example, for 2D tensors: +** If `dim=0`, `index` selects rows. +** If `dim=1`, `index` selects columns (for 2D tensors). + +`index` is a list of indices that will be converted into a tensor. + +Example: +``` +Input Tensor: +[[1, 2, 3], + [40, 50, 60], + [700, 800, 900]] + +index = "[0, 2]" +dim = 0 + +Output Tensor: +[[1, 2, 3], + [700, 800, 900]] +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | Int | +| index | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Indexing and Slicing Operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_int_create.md b/docs/reference/pt_int_create.md new file mode 100644 index 0000000..9f3d826 --- /dev/null +++ b/docs/reference/pt_int_create.md @@ -0,0 +1,18 @@ +# Pt Int Create +Creates a PyTorch tensor with 32-bit integer +using values entered in the text field. + +## Input +| Name | Data type | +|---|---| +| data | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_interpolate_by_scale_factor.md b/docs/reference/pt_interpolate_by_scale_factor.md new file mode 100644 index 0000000..ec781ac --- /dev/null +++ b/docs/reference/pt_interpolate_by_scale_factor.md @@ -0,0 +1,23 @@ +# Pt Interpolate By Scale Factor +Resizes a PyTorch tensor using interpolation by scale factor. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + +In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + +This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| scale_factor | Float | +| mode | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_interpolate_to_size.md b/docs/reference/pt_interpolate_to_size.md new file mode 100644 index 0000000..acec38d --- /dev/null +++ b/docs/reference/pt_interpolate_to_size.md @@ -0,0 +1,24 @@ +# Pt Interpolate To Size +Resizes a PyTorch tensor using interpolation. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + +In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + +This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| height | Int | +| width | Int | +| mode | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_le.md b/docs/reference/pt_le.md new file mode 100644 index 0000000..aee74d2 --- /dev/null +++ b/docs/reference/pt_le.md @@ -0,0 +1,18 @@ +# Pt Le +Tests whether elements in the first PyTorch tensor are less than or equal to the corresponding elements in the second tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Comparison operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_linspace.md b/docs/reference/pt_linspace.md new file mode 100644 index 0000000..df29f03 --- /dev/null +++ b/docs/reference/pt_linspace.md @@ -0,0 +1,23 @@ +# Pt Linspace +Creates a PyTorch tensor using `torch.linspace` with the specified start, end, and steps values. + +The start and end values are parsed from string inputs and converted to a float. +The steps value is converted to an int. + +## Input +| Name | Data type | +|---|---| +| start | String | +| end | String | +| steps | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_load_model.md b/docs/reference/pt_load_model.md new file mode 100644 index 0000000..0466004 --- /dev/null +++ b/docs/reference/pt_load_model.md @@ -0,0 +1,18 @@ +# Pt Load Model +A wrapper class for saving a PyTorch model. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| model_path | String | + +## Output +| Data type | +|---| +| Ptmodel | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_log.md b/docs/reference/pt_log.md new file mode 100644 index 0000000..f15a101 --- /dev/null +++ b/docs/reference/pt_log.md @@ -0,0 +1,17 @@ +# Pt Log +Computes the natural logarithm (log base e) of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_logical_and.md b/docs/reference/pt_logical_and.md new file mode 100644 index 0000000..10255b1 --- /dev/null +++ b/docs/reference/pt_logical_and.md @@ -0,0 +1,18 @@ +# Pt Logical And +Performs a logical AND operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Logical operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_logical_not.md b/docs/reference/pt_logical_not.md new file mode 100644 index 0000000..66616e3 --- /dev/null +++ b/docs/reference/pt_logical_not.md @@ -0,0 +1,17 @@ +# Pt Logical Not +Performs a logical NOT operation on a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Logical operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_logical_or.md b/docs/reference/pt_logical_or.md new file mode 100644 index 0000000..3519a38 --- /dev/null +++ b/docs/reference/pt_logical_or.md @@ -0,0 +1,18 @@ +# Pt Logical Or +Performs a logical OR operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Logical operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_logical_xor.md b/docs/reference/pt_logical_xor.md new file mode 100644 index 0000000..c67fe55 --- /dev/null +++ b/docs/reference/pt_logical_xor.md @@ -0,0 +1,18 @@ +# Pt Logical Xor +Performs a logical XOR operation on two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Logical operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_lt.md b/docs/reference/pt_lt.md new file mode 100644 index 0000000..b44c48b --- /dev/null +++ b/docs/reference/pt_lt.md @@ -0,0 +1,18 @@ +# Pt Lt +Tests whether elements in the first PyTorch tensor are less than the corresponding elements in the second tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Comparison operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_masked_select.md b/docs/reference/pt_masked_select.md new file mode 100644 index 0000000..80b9e15 --- /dev/null +++ b/docs/reference/pt_masked_select.md @@ -0,0 +1,34 @@ +# Pt Masked Select +Extracts elements from the input tensor whose corresponding value in `masked_tens` is `True`. + +Example: +``` +Input Tensor: +[[1, 2, 3], + [40, 50, 60], + [700, 800, 900]] + +Masked Tensor: +[[True, False, False], + [False, True, False], + [False, False, True]] + +Output Tensor: +[1, 50, 900] +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| masked_tens | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Indexing and Slicing Operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_matmul.md b/docs/reference/pt_matmul.md new file mode 100644 index 0000000..8536132 --- /dev/null +++ b/docs/reference/pt_matmul.md @@ -0,0 +1,18 @@ +# Pt Mat Mul +Performs matrix multiplication on two PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Matrix operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_max.md b/docs/reference/pt_max.md new file mode 100644 index 0000000..1f8b497 --- /dev/null +++ b/docs/reference/pt_max.md @@ -0,0 +1,24 @@ +# Pt Max +Computes the maximum values of a PyTorch tensor along the specified dimension(s). + +Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: +``` +0, [0], or (1, 2) +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_mean.md b/docs/reference/pt_mean.md new file mode 100644 index 0000000..e202099 --- /dev/null +++ b/docs/reference/pt_mean.md @@ -0,0 +1,26 @@ +# Pt Mean +Computes the mean of a PyTorch tensor along the specified dimension(s). + +This node supports only floating-point or complex tensors as input. + +Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: +``` +0, [0], or (1, 2) +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_median.md b/docs/reference/pt_median.md new file mode 100644 index 0000000..1d698d9 --- /dev/null +++ b/docs/reference/pt_median.md @@ -0,0 +1,29 @@ +# Pt Median +Computes the median of a PyTorch tensor along the specified dimension(s). + +Specify the dimension(s) in the `dim` field using an integer as shown below: +``` +0 +``` +or +``` +1 +``` +Note that PtMedian calls torch.median(), which returns the lower of the two numbers when the true median falls between them. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_min.md b/docs/reference/pt_min.md new file mode 100644 index 0000000..f0dc408 --- /dev/null +++ b/docs/reference/pt_min.md @@ -0,0 +1,24 @@ +# Pt Min +Computes the minimum values of a PyTorch tensor along the specified dimension(s). + +Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: +``` +0, [0], or (1, 2) +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_mm.md b/docs/reference/pt_mm.md new file mode 100644 index 0000000..c6524b9 --- /dev/null +++ b/docs/reference/pt_mm.md @@ -0,0 +1,18 @@ +# Pt Mm +Performs 2D matrix multiplication on two PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Matrix operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_mul.md b/docs/reference/pt_mul.md new file mode 100644 index 0000000..cda34b4 --- /dev/null +++ b/docs/reference/pt_mul.md @@ -0,0 +1,18 @@ +# Pt Mul +Multiplies two PyTorch tensors element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_ne.md b/docs/reference/pt_ne.md new file mode 100644 index 0000000..37e88c7 --- /dev/null +++ b/docs/reference/pt_ne.md @@ -0,0 +1,18 @@ +# Pt Ne +Tests whether two PyTorch tensors are not equal element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Comparison operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_neg.md b/docs/reference/pt_neg.md new file mode 100644 index 0000000..23d9114 --- /dev/null +++ b/docs/reference/pt_neg.md @@ -0,0 +1,17 @@ +# Pt Neg +Computes the negation of each element in a PyTorch tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_ones.md b/docs/reference/pt_ones.md new file mode 100644 index 0000000..5f42dc7 --- /dev/null +++ b/docs/reference/pt_ones.md @@ -0,0 +1,20 @@ +# Pt Ones +Creates a PyTorch tensor of ones using the size entered in the text field. +For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: +[2,3,96,32] + +## Input +| Name | Data type | +|---|---| +| size | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_pad.md b/docs/reference/pt_pad.md new file mode 100644 index 0000000..2353f26 --- /dev/null +++ b/docs/reference/pt_pad.md @@ -0,0 +1,23 @@ +# Pt Pad +Pads a PyTorch tensor to the specified size. Padded area will be black. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + +In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + +This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| height | Int | +| width | Int | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_permute.md b/docs/reference/pt_permute.md new file mode 100644 index 0000000..8fa187f --- /dev/null +++ b/docs/reference/pt_permute.md @@ -0,0 +1,25 @@ +# Pt Permute +Permutes the dimensions of a PyTorch tensor according to the specified order. +For example, if a tensor has shape (2, 3, 96, 32) and `new_axes="[0, 3, 1, 2]"`, +the output will have shape (2, 32, 3, 96). + +Specify the new axis order in the `new_axes` field using brackets, as shown below: +``` +[0, 3, 1, 2] +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| new_axes | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Transform + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_pow.md b/docs/reference/pt_pow.md new file mode 100644 index 0000000..1d717bf --- /dev/null +++ b/docs/reference/pt_pow.md @@ -0,0 +1,18 @@ +# Pt Pow +Raises one PyTorch tensor to the power of another element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_predict_classification_model.md b/docs/reference/pt_predict_classification_model.md new file mode 100644 index 0000000..217f61a --- /dev/null +++ b/docs/reference/pt_predict_classification_model.md @@ -0,0 +1,22 @@ +# Pt Predict Classification Model +Performs inference on input data. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| inputs | Tensor | +| class_id_to_name_map | Pydict | +| use_gpu | Boolean | + +## Output +| Data type | +|---| +| String | +| Int | +| Float | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_prod.md b/docs/reference/pt_prod.md new file mode 100644 index 0000000..f7d2f37 --- /dev/null +++ b/docs/reference/pt_prod.md @@ -0,0 +1,28 @@ +# Pt Prod +Computes the product of a PyTorch tensor along the specified dimension(s). + +Specify the dimension(s) in the `dim` field using an integer as shown below: +``` +0 +``` +or +``` +1 +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_rand.md b/docs/reference/pt_rand.md new file mode 100644 index 0000000..34ccd58 --- /dev/null +++ b/docs/reference/pt_rand.md @@ -0,0 +1,21 @@ +# Pt Rand +Creates a PyTorch tensor with values sampled from a uniform distribution +using the size entered in the text field. +For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: +[2,3,96,32] + +## Input +| Name | Data type | +|---|---| +| size | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_rand_int.md b/docs/reference/pt_rand_int.md new file mode 100644 index 0000000..16552cd --- /dev/null +++ b/docs/reference/pt_rand_int.md @@ -0,0 +1,22 @@ +# Pt Rand Int +Creates a PyTorch tensor filled with random integers within a specified range using the size entered in the text field. +For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32 with random integers in the range [0, 10), +enter [2,3,96,32] for size, 0 for min_value, and 10 for max_value. + +## Input +| Name | Data type | +|---|---| +| min_value | Int | +| max_value | Int | +| size | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_randn.md b/docs/reference/pt_randn.md new file mode 100644 index 0000000..8c2057b --- /dev/null +++ b/docs/reference/pt_randn.md @@ -0,0 +1,21 @@ +# Pt Randn +Creates a PyTorch tensor with values sampled from a standard normal distribution (mean=0, std=1) +using the size entered in the text field. +For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: +[2,3,96,32] + +## Input +| Name | Data type | +|---|---| +| size | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_remainder.md b/docs/reference/pt_remainder.md new file mode 100644 index 0000000..e4f68e6 --- /dev/null +++ b/docs/reference/pt_remainder.md @@ -0,0 +1,18 @@ +# Pt Remainder +Computes the element-wise remainder of division between two PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_reshape.md b/docs/reference/pt_reshape.md new file mode 100644 index 0000000..fecad29 --- /dev/null +++ b/docs/reference/pt_reshape.md @@ -0,0 +1,27 @@ +# Pt Reshape +Reshapes a PyTorch tensor into a specified shape using `torch.reshape()`. +The total number of elements must remain unchanged. + +For example, if a tensor has shape (2, 3, 4), and `new_shape="[6, 4]"`, +the output will have shape (6, 4). + +Use -1 to automatically infer a dimension, e.g.: +``` +[2, -1] +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| new_shape | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Transform + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_save_model.md b/docs/reference/pt_save_model.md new file mode 100644 index 0000000..da2d30c --- /dev/null +++ b/docs/reference/pt_save_model.md @@ -0,0 +1,18 @@ +# Pt Save Model +A wrapper class for saving a PyTorch model. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| model_path | String | + +## Output +| Data type | +|---| +| String | + +
+Category: IO + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_scatter.md b/docs/reference/pt_scatter.md new file mode 100644 index 0000000..719ea0c --- /dev/null +++ b/docs/reference/pt_scatter.md @@ -0,0 +1,24 @@ +# Pt Scatter +Generates a new tensor by replacing values at specified positions using an index tensor. + +* Target tensor (tens): Some of whose values are replaced. +* Source tensor (src): Values from this tensor replace a part or whole of the target tensor. +* Index tensor (index): Specifies the positions in the target tensor for replacement. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | Int | +| index | String | +| src | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Indexing and Slicing Operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_show_size.md b/docs/reference/pt_show_size.md new file mode 100644 index 0000000..8830fb1 --- /dev/null +++ b/docs/reference/pt_show_size.md @@ -0,0 +1,17 @@ +# Pt Show Size +Displays PyTorch Size object as a string. + +## Input +| Name | Data type | +|---|---| +| size | Ptsize | + +## Output +| Data type | +|---| +| String | + +
+Category: Display data + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_show_text.md b/docs/reference/pt_show_text.md new file mode 100644 index 0000000..40e397d --- /dev/null +++ b/docs/reference/pt_show_text.md @@ -0,0 +1,18 @@ +# Pt Show Text +Displays PyTorch tensor as a string. Note that the tensor is partially printed out when +the tensor contains more elements than the threshold set internally. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| String | + +
+Category: Display data + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_sin.md b/docs/reference/pt_sin.md new file mode 100644 index 0000000..158065b --- /dev/null +++ b/docs/reference/pt_sin.md @@ -0,0 +1,17 @@ +# Pt Sin +Computes the sine of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_sinh.md b/docs/reference/pt_sinh.md new file mode 100644 index 0000000..755828f --- /dev/null +++ b/docs/reference/pt_sinh.md @@ -0,0 +1,17 @@ +# Pt Sinh +Computes the hyperbolic sine of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_size.md b/docs/reference/pt_size.md new file mode 100644 index 0000000..a879754 --- /dev/null +++ b/docs/reference/pt_size.md @@ -0,0 +1,17 @@ +# Pt Size +Extracts the PyTorch Size object of a PyTorch tensor using the size() method. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Ptsize | + +
+Category: PyTorch wrapper - Size object support + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_size_create.md b/docs/reference/pt_size_create.md new file mode 100644 index 0000000..8aee4ab --- /dev/null +++ b/docs/reference/pt_size_create.md @@ -0,0 +1,17 @@ +# Pt Size Create +Creates a PyTorch Size using values entered in the text field. + +## Input +| Name | Data type | +|---|---| +| data | String | + +## Output +| Data type | +|---| +| Ptsize | + +
+Category: PyTorch wrapper - Size object support + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_size_to_numpy.md b/docs/reference/pt_size_to_numpy.md new file mode 100644 index 0000000..784063d --- /dev/null +++ b/docs/reference/pt_size_to_numpy.md @@ -0,0 +1,17 @@ +# Pt Size To Numpy +Converts PyTorch Size object to NumPy ndarray. + +## Input +| Name | Data type | +|---|---| +| sz | Ptsize | + +## Output +| Data type | +|---| +| Ndarray | + +
+Category: PyTorch wrapper - Size object support + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_size_to_string.md b/docs/reference/pt_size_to_string.md new file mode 100644 index 0000000..2e94aaa --- /dev/null +++ b/docs/reference/pt_size_to_string.md @@ -0,0 +1,17 @@ +# Pt Size To String +Converts PyTorch Size object to a Python string. + +## Input +| Name | Data type | +|---|---| +| sz | Ptsize | + +## Output +| Data type | +|---| +| String | + +
+Category: PyTorch wrapper - Size object support + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_sqrt.md b/docs/reference/pt_sqrt.md new file mode 100644 index 0000000..0b6165c --- /dev/null +++ b/docs/reference/pt_sqrt.md @@ -0,0 +1,17 @@ +# Pt Sqrt +Computes the square root of each element in a PyTorch tensor. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_squeeze.md b/docs/reference/pt_squeeze.md new file mode 100644 index 0000000..b11c993 --- /dev/null +++ b/docs/reference/pt_squeeze.md @@ -0,0 +1,20 @@ +# Pt Squeeze +Removes a dimension at the specified position in the input tensor if it is of size 1. + +Specifying a tuple of dimensions is not supported yet. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | Int | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Transform + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_std.md b/docs/reference/pt_std.md new file mode 100644 index 0000000..16ebc9b --- /dev/null +++ b/docs/reference/pt_std.md @@ -0,0 +1,28 @@ +# Pt Std +Computes the standard deviation of a PyTorch tensor along the specified dimension(s). + +Specify 1 to use Bessel's correction (N-1) for sample standard deviation. +Specify 0 to compute population standard deviation. + +Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: +``` +0, [0], or (1, 2) +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| correction | Int | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_sub.md b/docs/reference/pt_sub.md new file mode 100644 index 0000000..827a938 --- /dev/null +++ b/docs/reference/pt_sub.md @@ -0,0 +1,18 @@ +# Pt Sub +Subtracts one PyTorch tensor from another. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | +| tens_b | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Arithmetic operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_sum.md b/docs/reference/pt_sum.md new file mode 100644 index 0000000..99608c8 --- /dev/null +++ b/docs/reference/pt_sum.md @@ -0,0 +1,24 @@ +# Pt Sum +Computes the sum of a PyTorch tensor along the specified dimension(s). + +Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: +``` +0, [0], or (1, 2) +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_tan.md b/docs/reference/pt_tan.md new file mode 100644 index 0000000..a05b3a5 --- /dev/null +++ b/docs/reference/pt_tan.md @@ -0,0 +1,17 @@ +# Pt Tan +Computes the tangent of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_tanh.md b/docs/reference/pt_tanh.md new file mode 100644 index 0000000..eff2b90 --- /dev/null +++ b/docs/reference/pt_tanh.md @@ -0,0 +1,17 @@ +# Pt Tanh +Computes the hyperbolic tangent of a PyTorch tensor element-wise. + +## Input +| Name | Data type | +|---|---| +| tens_a | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Math operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_to_image.md b/docs/reference/pt_to_image.md new file mode 100644 index 0000000..a5f86e8 --- /dev/null +++ b/docs/reference/pt_to_image.md @@ -0,0 +1,17 @@ +# Pt To Image +Casts a PyTorch tensor as an Image tensor. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Image | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_to_image_transpose.md b/docs/reference/pt_to_image_transpose.md new file mode 100644 index 0000000..21af352 --- /dev/null +++ b/docs/reference/pt_to_image_transpose.md @@ -0,0 +1,17 @@ +# Pt To Image Transpose +Casts a PyTorch tensor as an Image tensor and transposes it from (C, H, W) to (H, W, C). For rank-4 inputs, the batch axis remains unchanged. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Image | + +
+Category: PyTorch wrapper - Image processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_to_latent.md b/docs/reference/pt_to_latent.md new file mode 100644 index 0000000..9a1278c --- /dev/null +++ b/docs/reference/pt_to_latent.md @@ -0,0 +1,17 @@ +# Pt To Latent +Casts a PyTorch tensor as a latent tensor. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Latent | + +
+Category: PyTorch wrapper - Tensor data conversion + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_to_numpy.md b/docs/reference/pt_to_numpy.md new file mode 100644 index 0000000..47821d3 --- /dev/null +++ b/docs/reference/pt_to_numpy.md @@ -0,0 +1,17 @@ +# Pt To Numpy +Converts PyTorch tensor to NumPy ndarray. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Ndarray | + +
+Category: PyTorch wrapper - Tensor data conversion + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_to_rgb_tensors.md b/docs/reference/pt_to_rgb_tensors.md new file mode 100644 index 0000000..aff0b37 --- /dev/null +++ b/docs/reference/pt_to_rgb_tensors.md @@ -0,0 +1,19 @@ +# Pt To Rgb Tensors +Splits a PyTorch tensor into R, G, and B tensors. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| Tensor | +| Tensor | +| Tensor | + +
+Category: PyTorch wrapper - Tensor data conversion + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_train_classification_model.md b/docs/reference/pt_train_classification_model.md new file mode 100644 index 0000000..061a2c4 --- /dev/null +++ b/docs/reference/pt_train_classification_model.md @@ -0,0 +1,30 @@ +# Pt Train Classification Model +Trains a classification model using a given dataset, optimizer, and number of epochs. + +early_stopping_rounds: Specifies the number of rounds to monitor the evaluation loss when early_stopping is enabled and validation data is specified. If the loss does not decrease compared to the current best loss within this period, training will be stopped. +output_best_val_model: Whether to outputs the model with the best val loss or the model from the last epoch. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| train_loader | Ptdataloader | +| optimizer | Ptoptimizer | +| epochs | Int | +| use_gpu | Boolean | +| early_stopping | Boolean | +| early_stopping_rounds | Int | +| output_best_val_model | Boolean | +| val_loader | Ptdataloader | + +## Output +| Data type | +|---| +| Ptmodel | +| Tensor | +| Tensor | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_unsqueeze.md b/docs/reference/pt_unsqueeze.md new file mode 100644 index 0000000..96bc5b1 --- /dev/null +++ b/docs/reference/pt_unsqueeze.md @@ -0,0 +1,19 @@ +# Pt Unsqueeze +Adds a singleton dimension at the specified position in the input tensor. +For example, if a tensor has shape (3, 4) and `dim=1`, the output will have shape (3, 1, 4). + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| dim | Int | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Transform + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_var.md b/docs/reference/pt_var.md new file mode 100644 index 0000000..383af73 --- /dev/null +++ b/docs/reference/pt_var.md @@ -0,0 +1,28 @@ +# Pt Var +Computes the variance of a PyTorch tensor along the specified dimension(s). + +Specify 1 to use Bessel's correction (N-1) for sample variance. +Specify 0 to compute population variance. + +Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: +``` +0, [0], or (1, 2) +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| correction | Int | +| dim | String | +| keepdim | Boolean | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Reduction operation & Summary statistics + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_view.md b/docs/reference/pt_view.md new file mode 100644 index 0000000..d366f3e --- /dev/null +++ b/docs/reference/pt_view.md @@ -0,0 +1,27 @@ +# Pt View +Reshapes a PyTorch tensor into a specified shape using `torch.view()`. +The total number of elements must remain unchanged. + +For example, if a tensor has shape (2, 3, 4), and `new_shape="[6, 4]"`, +the output will have shape (6, 4). + +Use -1 to automatically infer a dimension, e.g.: +``` +[2, -1] +``` + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | +| new_shape | String | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Transform + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_where.md b/docs/reference/pt_where.md new file mode 100644 index 0000000..b10ead9 --- /dev/null +++ b/docs/reference/pt_where.md @@ -0,0 +1,23 @@ +# Pt Where +Generates a new tensor by selecting values based on a condition tensor. + +* `condition_tens` (torch.Tensor): Boolean tensor that determines which values are selected. +* `true_tens` (torch.Tensor): Values to use where the condition is `True`. +* `false_tens` (torch.Tensor): Values to use where the condition is `False`. + +## Input +| Name | Data type | +|---|---| +| condition_tens | Tensor | +| true_tens | Tensor | +| false_tens | Tensor | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Indexing and Slicing Operations + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_zeros.md b/docs/reference/pt_zeros.md new file mode 100644 index 0000000..7019740 --- /dev/null +++ b/docs/reference/pt_zeros.md @@ -0,0 +1,20 @@ +# Pt Zeros +Creates a PyTorch tensor of zeros using the size entered in the text field. +For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: +[2,3,96,32] + +## Input +| Name | Data type | +|---|---| +| size | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: PyTorch wrapper - Tensor creation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptn_conv_model.md b/docs/reference/ptn_conv_model.md new file mode 100644 index 0000000..d881ea0 --- /dev/null +++ b/docs/reference/ptn_conv_model.md @@ -0,0 +1,45 @@ +# Ptn Conv Model +A convolutional model consisting of multiple convolutional layers. + +### Fields: +- `input_dim`: A string representing the input dimensions in the format "(C,H,W)". + Example: `"(3,28,28)"` for a 3-channel 28x28 image. + +- `penultimate_dim`: An integer specifying the number of features before the output layer. If you specify 0, the number is computed internally. + Default: `0` + +- `output_dim`: An integer specifying the number of output features, which should match the number of target classes in classification. + Default: `10`, Min: `1`, Max: `1e6`. + +- `channel_list`: A string representing a Python list of integers specifying the number of channels per layer excluding the channel for the input (e.g. 3 for the color image). + Example: `"[32,64,128,256,512]"`. + +- `kernel_size_list`: A string representing a Python list of kernel sizes per layer. + Example: `"[3,3,3,3,1]"`. + +- `padding_list`: A string representing a Python list of padding values per layer. + Example: `"[1,1,1,1,0]"`. + +- `downsample_list`: A string representing a Python list of boolean values indicating whether to downsample at each layer. + Example: `"[True,True,True,True,False]"`. + +## Input +| Name | Data type | +|---|---| +| input_dim | String | +| penultimate_dim | Int | +| output_dim | Int | +| channel_list | String | +| kernel_size_list | String | +| padding_list | String | +| downsample_list | String | + +## Output +| Data type | +|---| +| Ptmodel | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptn_linear_model.md b/docs/reference/ptn_linear_model.md new file mode 100644 index 0000000..a9788fa --- /dev/null +++ b/docs/reference/ptn_linear_model.md @@ -0,0 +1,35 @@ +# Ptn Linear Model +A linear model consisting of dense layers. + +### Fields: +- `dim_list`: A string representing a Python list of layer dimensions. + It should contain the input dimension for the first layer and the output dimensions + for all layers. + + Example for a two-layer network: + - Input data: 784 + - Layer 1: 784 → 128 + - Layer 2: 128 → 10 + - Specify `"[784,128,10]"` + +- `bias_list`: A string representing a Python list of bias usage (True/False) per layer. + Example: "[True,False]" + +- `num_layers`: The number of layers in the model. + +## Input +| Name | Data type | +|---|---| +| dim_list | String | +| bias_list | String | +| num_layers | Int | + +## Output +| Data type | +|---| +| Ptmodel | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptn_resnet_model.md b/docs/reference/ptn_resnet_model.md new file mode 100644 index 0000000..25e0a84 --- /dev/null +++ b/docs/reference/ptn_resnet_model.md @@ -0,0 +1,28 @@ +# Ptn Resnet Model +A Resnet model consisting of multiple Resnet layers. + +### Fields: +- `input_dim`: A string representing the input dimensions in the format "(C,H,W)". + Example: `"(3,32,32)"` for a 3-channel 28x28 image. + +- `output_dim`: An integer specifying the number of output features, which should match the number of target classes in classification. + Default: `10`, Min: `1`, Max: `1e6`. + +- `num_blocks`: Number of Resnet blocks for 64 channel blocks. Same number of blocks will be created for 128 channel and 256 channel blocks. + +## Input +| Name | Data type | +|---|---| +| input_dim | String | +| output_dim | Int | +| num_blocks | Int | + +## Output +| Data type | +|---| +| Ptmodel | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptn_resnet_model_def.md b/docs/reference/ptn_resnet_model_def.md new file mode 100644 index 0000000..588c8e2 --- /dev/null +++ b/docs/reference/ptn_resnet_model_def.md @@ -0,0 +1,7 @@ +# Resnet Model + + +
+Category: Miscellaneous + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pto_adam.md b/docs/reference/pto_adam.md new file mode 100644 index 0000000..257420e --- /dev/null +++ b/docs/reference/pto_adam.md @@ -0,0 +1,20 @@ +# Pto Adam +Instantiates the Adam optimizer. + +## Input +| Name | Data type | +|---|---| +| model | Ptmodel | +| learning_rate | Float | +| beta1 | Float | +| beta2 | Float | + +## Output +| Data type | +|---| +| Ptoptimizer | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptv_dataset.md b/docs/reference/ptv_dataset.md new file mode 100644 index 0000000..1258c76 --- /dev/null +++ b/docs/reference/ptv_dataset.md @@ -0,0 +1,51 @@ +# Ptv Dataset +A Torchvision Dataset class wrapper. + +Parameters + +**name** + Specify a dataset class name such as MNIST, FashionMNIST in string. + This will be converted to a dataset class internally. + +**download** + Set to True if you want to download. + +**root** + Specify the root directory of the downloaded dataset relative to the + dataset directory of this extension, or specify the absolute path. + If you leave as a blank, the dataset will be downloaded under the `datasets` directory + of this extension. + +**transform** + Data transforms e.g. transform to PyTorch tensor, normalize image. + Plug in the PTV Transforms node that contains Torchvision Transforms functionality. + +**parameters** + Specify other parameters in Python dict format. + For example, if you want to set Train parameter to True to download the train set, specify: + ``` + {"train": True} + ``` + or if you want to download the test set, specify: + ``` + {"train": False} + ``` + +## Input +| Name | Data type | +|---|---| +| name | String | +| download | Boolean | +| root | String | +| transform | Ptvtransform | +| parameters | String | + +## Output +| Data type | +|---| +| Ptvdataset | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptv_dataset_len.md b/docs/reference/ptv_dataset_len.md new file mode 100644 index 0000000..47ca57a --- /dev/null +++ b/docs/reference/ptv_dataset_len.md @@ -0,0 +1,17 @@ +# Ptv Dataset Len +A wrapper class that calls Python len on a dataset. + +## Input +| Name | Data type | +|---|---| +| dataset | Ptvdataset | + +## Output +| Data type | +|---| +| Int | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptv_dataset_loader.md b/docs/reference/ptv_dataset_loader.md new file mode 100644 index 0000000..ee13ac5 --- /dev/null +++ b/docs/reference/ptv_dataset_loader.md @@ -0,0 +1,60 @@ +# Ptv Dataset Loader +A node to combine the dataset and data loader into a single node. + +Transform is internally called to convert input data to PyTorch tensors. + +Parameters + +**name** + Specify the dataset class name such as MNIST, FashionMNIST in string. + This will be converted to a dataset class internally. + +**download** + Set to True if you want to download. + +**root** + Specify the root directory of the downloaded dataset relative to the + dataset directory of this extension, or specify the absolute path. + If you leave as a blank, the dataset will be downloaded under the `datasets` directory + of this extension. + +**batch_size** + The number of samples per batch. + +**shuffle** + If True, shuffles the dataset before each epoch. + +**dataset parameters** + Specify other parameters in Python dict format. + For example, if you want to set Train parameter to True to download the train set, specify: + ``` + {"train": True} + ``` + or if you want to download the test set, specify: + ``` + {"train": False} + ``` + +**load parameters** + Additional parameters in Python dictionary format. + +## Input +| Name | Data type | +|---|---| +| name | String | +| download | Boolean | +| root | String | +| batch_size | Int | +| shuffle | Boolean | +| dataset_parameters | String | +| load_parameters | String | + +## Output +| Data type | +|---| +| Ptdataloader | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptv_image_folder_dataset.md b/docs/reference/ptv_image_folder_dataset.md new file mode 100644 index 0000000..b973b55 --- /dev/null +++ b/docs/reference/ptv_image_folder_dataset.md @@ -0,0 +1,28 @@ +# Ptv Image Folder Dataset +A Torchvision ImageFolder Dataset class wrapper. + +Parameters + +**root** + Specify the root directory that contains subdirectories which of which contains images for each class. + The subdirectory name needs to be named after each class. + +**transform** + Data transforms e.g. transform to PyTorch tensor, normalize image. + Plug in the PTV Transforms node that contains Torchvision Transforms functionality. + +## Input +| Name | Data type | +|---|---| +| root | String | +| transform | Ptvtransform | + +## Output +| Data type | +|---| +| Ptvdataset | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptv_transforms_resize.md b/docs/reference/ptv_transforms_resize.md new file mode 100644 index 0000000..159626f --- /dev/null +++ b/docs/reference/ptv_transforms_resize.md @@ -0,0 +1,18 @@ +# Ptv Transforms Resize +Resizes and transforms elements of dataset to PyTorch tensors. + +## Input +| Name | Data type | +|---|---| +| height | Int | +| width | Int | + +## Output +| Data type | +|---| +| Ptvtransform | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/ptv_transforms_to_tensor.md b/docs/reference/ptv_transforms_to_tensor.md new file mode 100644 index 0000000..e1ca095 --- /dev/null +++ b/docs/reference/ptv_transforms_to_tensor.md @@ -0,0 +1,12 @@ +# Ptv Transforms To Tensor +Transforms elements of dataset to PyTorch tensors. + +## Output +| Data type | +|---| +| Ptvtransform | + +
+Category: PyTorch wrapper - Training + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/examples/workflows/dog_cat_classification_eval_model.json b/examples/workflows/dog_cat_classification_eval_model.json new file mode 100644 index 0000000..aff7eff --- /dev/null +++ b/examples/workflows/dog_cat_classification_eval_model.json @@ -0,0 +1 @@ +{"last_node_id":54,"last_link_id":67,"nodes":[{"id":49,"type":"PtDataLoader","pos":[2664.0126953125,2004.834716796875],"size":[308.1214599609375,135.3099822998047],"flags":{},"order":5,"mode":0,"inputs":[{"name":"dataset","type":"PTVDATASET","link":64}],"outputs":[{"name":"PTDATALOADER","type":"PTDATALOADER","links":[65],"slot_index":0}],"properties":{"Node name for S&R":"PtDataLoader"},"widgets_values":[8,false,"{\"num_workers\":1}"]},{"id":50,"type":"PtvImageFolderDataset","pos":[2324.53857421875,2006.695068359375],"size":[315,58],"flags":{},"order":3,"mode":0,"inputs":[{"name":"transform","type":"PTVTRANSFORM","link":67,"shape":7}],"outputs":[{"name":"PTVDATASET","type":"PTVDATASET","links":[64],"slot_index":0}],"properties":{"Node name for S&R":"PtvImageFolderDataset"},"widgets_values":["/container/transfer/datasets/dog_and_cat/dog_and_cat/val/"]},{"id":53,"type":"PtvTransformsResize","pos":[1959.0155029296875,2007.09521484375],"size":[315,82],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"PTVTRANSFORM","type":"PTVTRANSFORM","links":[67],"slot_index":0}],"properties":{"Node name for S&R":"PtvTransformsResize"},"widgets_values":[256,256]},{"id":48,"type":"PtnConvModel","pos":[2666.497314453125,1718.6485595703125],"size":[315,202],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"PTMODEL","type":"PTMODEL","links":[62],"slot_index":0}],"properties":{"Node name for S&R":"PtnConvModel"},"widgets_values":["(3,256,256)",0,2,"[32,64,128,256,512]","[3,3,3,3,3]","[1,1,1,1,1]","[True,True,True,True,True]"]},{"id":47,"type":"PtEvaluateClassificationModel","pos":[3073.28466796875,1998.2191162109375],"size":[268.79998779296875,148.22767639160156],"flags":{},"order":6,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":55},{"name":"data_loader","type":"PTDATALOADER","link":65}],"outputs":[{"name":"accuracy","type":"FLOAT","links":[66],"slot_index":0},{"name":"precision","type":"FLOAT","links":[58],"slot_index":1},{"name":"recall","type":"FLOAT","links":[59],"slot_index":2},{"name":"f1","type":"FLOAT","links":[60],"slot_index":3},{"name":"num_samples","type":"INT","links":[61],"slot_index":4}],"properties":{"Node name for S&R":"PtEvaluateClassificationModel"},"widgets_values":[true]},{"id":33,"type":"CDAShowInt","pos":[3507.768310546875,2136.57861328125],"size":[210,79.09933471679688],"flags":{},"order":11,"mode":0,"inputs":[{"name":"int_scalar","type":"INT","link":61,"widget":{"name":"int_scalar"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"title":"Num Samples","properties":{"Node name for S&R":"CDAShowInt"},"widgets_values":[0,"300"]},{"id":30,"type":"CDAShowFloat","pos":[3507.768310546875,1986.105712890625],"size":[210,76],"flags":{},"order":9,"mode":0,"inputs":[{"name":"float_scalar","type":"FLOAT","link":59,"widget":{"name":"float_scalar"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"title":"Recall","properties":{"Node name for S&R":"CDAShowFloat"},"widgets_values":[0,"0.9"]},{"id":31,"type":"CDAShowFloat","pos":[3768.4755859375,1980.9920654296875],"size":[210,76],"flags":{},"order":10,"mode":0,"inputs":[{"name":"float_scalar","type":"FLOAT","link":60,"widget":{"name":"float_scalar"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"title":"F1","properties":{"Node name for S&R":"CDAShowFloat"},"widgets_values":[0,"0.8997817469155049"]},{"id":29,"type":"CDAShowFloat","pos":[3768.4755859375,1834.9833984375],"size":[210,76],"flags":{},"order":8,"mode":0,"inputs":[{"name":"float_scalar","type":"FLOAT","link":58,"widget":{"name":"float_scalar"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"title":"Precision","properties":{"Node name for S&R":"CDAShowFloat"},"widgets_values":[0,"0.9035150645624104"]},{"id":52,"type":"CDAShowFloat","pos":[3507.768310546875,1834.7109375],"size":[210,76],"flags":{},"order":7,"mode":0,"inputs":[{"name":"float_scalar","type":"FLOAT","link":66,"widget":{"name":"float_scalar"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"title":"Accuracy","properties":{"Node name for S&R":"CDAShowFloat"},"widgets_values":[0,"0.9"]},{"id":23,"type":"PtLoadModel","pos":[3075.703369140625,1723.319580078125],"size":[261.13433837890625,74.19380950927734],"flags":{},"order":4,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":62}],"outputs":[{"name":"model","type":"PTMODEL","links":[55],"slot_index":0}],"properties":{"Node name for S&R":"PtLoadModel"},"widgets_values":["dog_cat.pt"]},{"id":54,"type":"MarkdownNote","pos":[2328.715576171875,1839.947509765625],"size":[313.0150451660156,99.62860870361328],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Load val data\nYou need set the root field to point to the **val** directory of the dataset you downloaded."],"color":"#322","bgcolor":"#533"}],"links":[[55,23,0,47,0,"PTMODEL"],[58,47,1,29,0,"FLOAT"],[59,47,2,30,0,"FLOAT"],[60,47,3,31,0,"FLOAT"],[61,47,4,33,0,"INT"],[62,48,0,23,0,"PTMODEL"],[64,50,0,49,0,"PTVDATASET"],[65,49,0,47,1,"PTDATALOADER"],[66,47,0,52,0,"FLOAT"],[67,53,0,50,0,"PTVTRANSFORM"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.34289106510190753,"offset":[-1247.259007394664,-447.540790015054]},"node_versions":{"data-analysis":"db92cda5f5a1e611e15ca6f2531ff8585a5607a5"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/examples/workflows/dog_cat_classification_prediction.json b/examples/workflows/dog_cat_classification_prediction.json new file mode 100644 index 0000000..187c32a --- /dev/null +++ b/examples/workflows/dog_cat_classification_prediction.json @@ -0,0 +1 @@ +{"last_node_id":74,"last_link_id":97,"nodes":[{"id":73,"type":"PtFromImageTranspose","pos":[1488.2567138671875,1628.712646484375],"size":[289.79998779296875,26],"flags":{},"order":4,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":95}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[96],"slot_index":0}],"properties":{"Node name for S&R":"PtFromImageTranspose"}},{"id":74,"type":"PtInterpolateToSize","pos":[1801.049560546875,1628.712646484375],"size":[315,106],"flags":{},"order":5,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":96}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[97],"slot_index":0}],"properties":{"Node name for S&R":"PtInterpolateToSize"},"widgets_values":[256,256,"nearest-exact"]},{"id":69,"type":"PtSqueeze","pos":[2206.266357421875,1639.8197021484375],"size":[210,60.5162467956543],"flags":{},"order":6,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":97}],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[88],"slot_index":0}],"properties":{"Node name for S&R":"PtSqueeze"},"widgets_values":[0]},{"id":55,"type":"PtPredictClassificationModel","pos":[2482.278564453125,1626.3341064453125],"size":[405.5999755859375,98],"flags":{},"order":7,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":70},{"name":"inputs","type":"TENSOR","link":88},{"name":"class_id_to_name_map","type":"PYDICT","link":69}],"outputs":[{"name":"class_name","type":"STRING","links":[74],"slot_index":0},{"name":"class_id","type":"INT","links":null},{"name":"probability","type":"FLOAT","links":[93],"slot_index":2}],"properties":{"Node name for S&R":"PtPredictClassificationModel"},"widgets_values":[true]},{"id":56,"type":"PyStringToDict","pos":[2206.266357421875,1774.539306640625],"size":[210,81.30076599121094],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"PYDICT","type":"PYDICT","links":[69],"slot_index":0}],"properties":{"Node name for S&R":"PyStringToDict"},"widgets_values":["{0:\"cat\", 1:\"dog\"}"]},{"id":60,"type":"PandasShowText","pos":[2970.7451171875,1629.65576171875],"size":[221.17684936523438,76],"flags":{},"order":8,"mode":0,"inputs":[{"name":"text","type":"STRING","link":74,"widget":{"name":"text"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowText"},"widgets_values":["","cat"]},{"id":72,"type":"CDAShowFloat","pos":[2976.763916015625,1770.6387939453125],"size":[210,76],"flags":{},"order":9,"mode":0,"inputs":[{"name":"float_scalar","type":"FLOAT","link":93,"widget":{"name":"float_scalar"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"CDAShowFloat"},"widgets_values":[0,"0.9118371605873108"]},{"id":23,"type":"PtLoadModel","pos":[2206.266357421875,1482.663330078125],"size":[210,75.45188903808594],"flags":{},"order":3,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":62}],"outputs":[{"name":"model","type":"PTMODEL","links":[70],"slot_index":0}],"properties":{"Node name for S&R":"PtLoadModel"},"widgets_values":["dog_cat.pt"]},{"id":57,"type":"LoadImage","pos":[1135.1787109375,1628.712646484375],"size":[315,314],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[95],"slot_index":0},{"name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["cat.jpg","image"]},{"id":48,"type":"PtnConvModel","pos":[1801.049560546875,1306.3406982421875],"size":[315,202],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"PTMODEL","type":"PTMODEL","links":[62],"slot_index":0}],"properties":{"Node name for S&R":"PtnConvModel"},"widgets_values":["(3,256,256)",0,2,"[32,64,128,256,512]","[3,3,3,3,3]","[1,1,1,1,1]","[True,True,True,True,True]"]}],"links":[[62,48,0,23,0,"PTMODEL"],[69,56,0,55,2,"PYDICT"],[70,23,0,55,0,"PTMODEL"],[74,55,0,60,0,"STRING"],[88,69,0,55,1,"TENSOR"],[93,55,2,72,0,"FLOAT"],[95,57,0,73,0,"IMAGE"],[96,73,0,74,0,"TENSOR"],[97,74,0,69,0,"TENSOR"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.6339486400897979,"offset":[-879.509850098028,-1085.7559202042573]},"node_versions":{"data-analysis":"fd6fa94b931cd094ce5766852f5914d7291cb464","comfy-core":"v0.3.10-40-gd0f3752"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/examples/workflows/dog_cat_classification_train_model.json b/examples/workflows/dog_cat_classification_train_model.json new file mode 100644 index 0000000..706b774 --- /dev/null +++ b/examples/workflows/dog_cat_classification_train_model.json @@ -0,0 +1 @@ +{"last_node_id":86,"last_link_id":95,"nodes":[{"id":11,"type":"PtoAdam","pos":[3106.311767578125,1425.00830078125],"size":[265.8851013183594,108.32855224609375],"flags":{},"order":16,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":89}],"outputs":[{"name":"PTOPTIMIZER","type":"PTOPTIMIZER","links":[68],"slot_index":0}],"properties":{"Node name for S&R":"PtoAdam"},"widgets_values":[0.0001,0.9,0.999]},{"id":72,"type":"PtDataLoader","pos":[2583.5888671875,1626.235595703125],"size":[400,200],"flags":{},"order":20,"mode":0,"inputs":[{"name":"dataset","type":"PTVDATASET","link":86}],"outputs":[{"name":"PTDATALOADER","type":"PTDATALOADER","links":[87],"slot_index":0}],"properties":{"Node name for S&R":"PtDataLoader"},"widgets_values":[8,true,"{\"num_workers\":1}"]},{"id":19,"type":"PtSaveModel","pos":[3464.489501953125,1694.661376953125],"size":[280.30267333984375,68.31287384033203],"flags":{},"order":23,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":71}],"outputs":[{"name":"model full path","type":"STRING","links":[70],"slot_index":0}],"properties":{"Node name for S&R":"PtSaveModel"},"widgets_values":["dog_cat.pt"]},{"id":74,"type":"PtvImageFolderDataset","pos":[2228.7578125,1923.7598876953125],"size":[315,58],"flags":{},"order":18,"mode":0,"inputs":[{"name":"transform","type":"PTVTRANSFORM","link":95,"shape":7}],"outputs":[{"name":"PTVDATASET","type":"PTVDATASET","links":[91],"slot_index":0}],"properties":{"Node name for S&R":"PtvImageFolderDataset"},"widgets_values":["/container/transfer/datasets/dog_and_cat/dog_and_cat/val/"]},{"id":71,"type":"PtnConvModel","pos":[2607.428955078125,1271.409912109375],"size":[315,202],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"PTMODEL","type":"PTMODEL","links":[88,89],"slot_index":0}],"properties":{"Node name for S&R":"PtnConvModel"},"widgets_values":["(3,256,256)",0,2,"[32,64,128,256,512]","[3,3,3,3,3]","[1,1,1,1,1]","[True,True,True,True,True]"]},{"id":53,"type":"MarkdownNote","pos":[2219.2607421875,1485.9119873046875],"size":[310.71795654296875,94.28345489501953],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Load train data\nYou need set the root field to point to the **train** directory of the dataset you downloaded."],"color":"#322","bgcolor":"#533"},{"id":76,"type":"MarkdownNote","pos":[2014.2506103515625,926.4641723632812],"size":[436.95391845703125,69.52714538574219],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Dog Cat Classification Model Training"],"color":"#432","bgcolor":"#653"},{"id":78,"type":"PtvTransformsResize","pos":[1820.2008056640625,1781.384521484375],"size":[315,82],"flags":{},"order":3,"mode":0,"inputs":[],"outputs":[{"name":"PTVTRANSFORM","type":"PTVTRANSFORM","links":[94,95],"slot_index":0}],"properties":{"Node name for S&R":"PtvTransformsResize"},"widgets_values":[256,256]},{"id":52,"type":"MarkdownNote","pos":[2605.098876953125,1119.3177490234375],"size":[308.6709289550781,100.76033782958984],"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Model set up\n\nNo need to edit this node unless you are familiar with machine learning"],"color":"#232","bgcolor":"#353"},{"id":79,"type":"MarkdownNote","pos":[1816.8997802734375,1627.8802490234375],"size":[317.4327697753906,98.5698471069336],"flags":{},"order":5,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Dataset transformation\n\nNo need to edit this node unless you are familiar with machine learning. This resizes input data to 256px x 256px."],"color":"#232","bgcolor":"#353"},{"id":75,"type":"PtDataLoader","pos":[2588.29736328125,1922.6170654296875],"size":[400,200],"flags":{},"order":21,"mode":0,"inputs":[{"name":"dataset","type":"PTVDATASET","link":91}],"outputs":[{"name":"PTDATALOADER","type":"PTDATALOADER","links":[93],"slot_index":0}],"properties":{"Node name for S&R":"PtDataLoader"},"widgets_values":[8,false,"{\"num_workers\":1}"]},{"id":51,"type":"PandasShowText","pos":[3845.79296875,1686.58056640625],"size":[499.0397033691406,76],"flags":{},"order":26,"mode":0,"inputs":[{"name":"text","type":"STRING","link":70,"widget":{"name":"text"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowText"},"widgets_values":["","/home/dockeruser/comfy/ComfyUI/custom_nodes/data-analysis/models/dog_cat.pt"]},{"id":58,"type":"PandasCreateFromTensor","pos":[3487.738037109375,2425.4267578125],"size":[315,26],"flags":{},"order":25,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":73}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[75],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"},"widgets_values":[]},{"id":64,"type":"PandasCreateFromTensor","pos":[3844.351806640625,2508.688720703125],"size":[315,26],"flags":{},"order":19,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":78}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[77],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"},"widgets_values":[]},{"id":65,"type":"PandasHorizontalConcat","pos":[4218.2119140625,2380.0498046875],"size":[315,78],"flags":{},"order":28,"mode":0,"inputs":[{"name":"a_dataframe","type":"DATAFRAME","link":79},{"name":"b_dataframe","type":"DATAFRAME","link":77}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[80,81],"slot_index":0}],"properties":{"Node name for S&R":"PandasHorizontalConcat"},"widgets_values":[true]},{"id":61,"type":"PandasRenameAdvanced","pos":[4608.04443359375,2380.0498046875],"size":[238.48175048828125,131.16506958007812],"flags":{},"order":30,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":81}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[82],"slot_index":0}],"properties":{"Node name for S&R":"PandasRenameAdvanced"},"widgets_values":["columns","{0:\"train\",1:\"val\",2:\"epoch\"}"]},{"id":60,"type":"SNSLine","pos":[4918.4609375,2380.0498046875],"size":[315,250],"flags":{},"order":31,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":82}],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[84],"slot_index":0}],"properties":{"Node name for S&R":"SNSLine"},"widgets_values":["epoch","train,val","Loss","Epoch","Cross entropy loss","train,val",true,"",""]},{"id":68,"type":"SaveImage","pos":[5326.560546875,2380.0498046875],"size":[315,270],"flags":{},"order":32,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":84}],"outputs":[],"properties":{"Node name for S&R":"SaveImage"},"widgets_values":["dog_cat_train"]},{"id":57,"type":"PandasCreateFromTensor","pos":[3483.9892578125,2346.20166015625],"size":[315,26],"flags":{},"order":24,"mode":0,"inputs":[{"name":"tens","type":"TENSOR","link":72}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[74],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreateFromTensor"},"widgets_values":[]},{"id":83,"type":"MarkdownNote","pos":[2943.528564453125,925.8107299804688],"size":[336.614990234375,70.34814453125],"flags":{},"order":6,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["## Yellow nodes may require adjustment"],"color":"#432","bgcolor":"#653"},{"id":80,"type":"MarkdownNote","pos":[3106.311767578125,1250.9371337890625],"size":[257.6202392578125,106.56401062011719],"flags":{},"order":7,"mode":2,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Optimizer\nYou may need to adjust the learning rate but starts with the number below."],"color":"#432","bgcolor":"#653"},{"id":50,"type":"PtTrainClassificationModel","pos":[3106.311767578125,1663.041748046875],"size":[265.619873046875,145.1302490234375],"flags":{},"order":22,"mode":0,"inputs":[{"name":"model","type":"PTMODEL","link":88},{"name":"train_loader","type":"PTDATALOADER","link":87},{"name":"optimizer","type":"PTOPTIMIZER","link":68},{"name":"val_loader","type":"PTDATALOADER","link":93,"shape":7}],"outputs":[{"name":"Model","type":"PTMODEL","links":[71],"slot_index":0},{"name":"train loss","type":"TENSOR","links":[72],"slot_index":1},{"name":"val loss","type":"TENSOR","links":[73],"slot_index":2}],"properties":{"Node name for S&R":"PtTrainClassificationModel"},"widgets_values":[20,true]},{"id":84,"type":"MarkdownNote","pos":[2596.4189453125,2210.809326171875],"size":[387.0498352050781,100.52718353271484],"flags":{},"order":8,"mode":2,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Data loading\nIf GPU throws OOM, make the batch_size smaller."],"color":"#432","bgcolor":"#653"},{"id":82,"type":"MarkdownNote","pos":[2944.823486328125,805.5132446289062],"size":[326.2669677734375,67.76112365722656],"flags":{},"order":9,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["## Red nodes require editing"],"color":"#322","bgcolor":"#533"},{"id":77,"type":"MarkdownNote","pos":[2230.19091796875,2034.1849365234375],"size":[313.0150451660156,99.62860870361328],"flags":{},"order":10,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Load val data\nYou need set the root field to point to the **val** directory of the dataset you downloaded."],"color":"#322","bgcolor":"#533"},{"id":62,"type":"PtArange","pos":[3481.524169921875,2515.711669921875],"size":[315,130],"flags":{},"order":11,"mode":0,"inputs":[],"outputs":[{"name":"TENSOR","type":"TENSOR","links":[78],"slot_index":0}],"properties":{"Node name for S&R":"PtArange"},"widgets_values":["0","20","","float32"],"color":"#432","bgcolor":"#653"},{"id":55,"type":"MarkdownNote","pos":[3106.311767578125,1861.1396484375],"size":[265.81390380859375,103.27005767822266],"flags":{},"order":12,"mode":2,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Train\nYou may need to adjust epochs but start with 20."],"color":"#432","bgcolor":"#653"},{"id":85,"type":"MarkdownNote","pos":[3485.391357421875,2688.060791015625],"size":[304.5123596191406,100.34938049316406],"flags":{},"order":13,"mode":2,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Update \"end\" field\nIf you change the epochs, change the end field to match."],"color":"#432","bgcolor":"#653"},{"id":66,"type":"PandasShowDataFrame","pos":[4215.47265625,2533.793701171875],"size":[315.237060546875,414.686279296875],"flags":{},"order":29,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":80}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[" 0 1 2\n0 0.686489 0.680127 0.0\n1 0.621646 0.571015 1.0\n2 0.499781 0.471638 2.0\n3 0.382664 0.404523 3.0\n4 0.289659 0.329657 4.0\n5 0.224341 0.292451 5.0\n6 0.160335 0.291596 6.0\n7 0.110199 0.321644 7.0\n8 0.091624 0.270642 8.0\n9 0.052231 0.221975 9.0\n10 0.048100 0.281967 10.0\n11 0.022782 0.289861 11.0\n12 0.013045 0.305584 12.0\n13 0.013724 0.307031 13.0\n14 0.020037 0.271379 14.0\n15 0.031278 0.310788 15.0\n16 0.012516 0.338837 16.0\n17 0.017392 0.298860 17.0\n18 0.001674 0.316514 18.0\n19 0.000855 0.358205 19.0"]},{"id":59,"type":"PandasHorizontalConcat","pos":[3849.372802734375,2380.0498046875],"size":[315,78],"flags":{},"order":27,"mode":0,"inputs":[{"name":"a_dataframe","type":"DATAFRAME","link":74},{"name":"b_dataframe","type":"DATAFRAME","link":75}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[79],"slot_index":0}],"properties":{"Node name for S&R":"PandasHorizontalConcat"},"widgets_values":[true]},{"id":86,"type":"MarkdownNote","pos":[3453.1630859375,2150.53857421875],"size":[1160.256103515625,64.83271026611328],"flags":{},"order":14,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# This area is to show how training went. Initially, it's OK if you don't understand the meaning."],"color":"#232","bgcolor":"#353"},{"id":69,"type":"PtvImageFolderDataset","pos":[2220.0927734375,1629.250732421875],"size":[315,58],"flags":{},"order":17,"mode":0,"inputs":[{"name":"transform","type":"PTVTRANSFORM","link":94,"shape":7}],"outputs":[{"name":"PTVDATASET","type":"PTVDATASET","links":[86],"slot_index":0}],"properties":{"Node name for S&R":"PtvImageFolderDataset"},"widgets_values":["/container/transfer/datasets/dog_and_cat/dog_and_cat/train/"]},{"id":56,"type":"MarkdownNote","pos":[3464.170166015625,1519.1202392578125],"size":[285.6708679199219,95.45172882080078],"flags":{},"order":15,"mode":2,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# Save model\nUpdate the model path if needed.\nThis is saved under custom_nodes/data-analysis/models"],"color":"#432","bgcolor":"#653"}],"links":[[68,11,0,50,2,"PTOPTIMIZER"],[70,19,0,51,0,"STRING"],[71,50,0,19,0,"PTMODEL"],[72,50,1,57,0,"TENSOR"],[73,50,2,58,0,"TENSOR"],[74,57,0,59,0,"DATAFRAME"],[75,58,0,59,1,"DATAFRAME"],[77,64,0,65,1,"DATAFRAME"],[78,62,0,64,0,"TENSOR"],[79,59,0,65,0,"DATAFRAME"],[80,65,0,66,0,"DATAFRAME"],[81,65,0,61,0,"DATAFRAME"],[82,61,0,60,0,"DATAFRAME"],[84,60,0,68,0,"IMAGE"],[86,69,0,72,0,"PTVDATASET"],[87,72,0,50,1,"PTDATALOADER"],[88,71,0,50,0,"PTMODEL"],[89,71,0,11,0,"PTMODEL"],[91,74,0,75,0,"PTVDATASET"],[93,75,0,50,3,"PTDATALOADER"],[94,78,0,69,0,"PTVTRANSFORM"],[95,78,0,74,0,"PTVTRANSFORM"]],"groups":[{"id":1,"title":"Training metrics","bounding":[3447.499755859375,2242.806884765625,2284.0107421875,790.1612548828125],"color":"#8A8","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":0.3736662716119983,"offset":[-975.7677617863449,-688.047741909445]},"node_versions":{"data-analysis":"db92cda5f5a1e611e15ca6f2531ff8585a5607a5","comfy-core":"v0.3.10-40-gd0f3752"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_abs.py b/modules/pytorch_wrapper/pt_abs.py new file mode 100644 index 0000000..3453cee --- /dev/null +++ b/modules/pytorch_wrapper/pt_abs.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtAbs: + """ + Computes the absolute value of each element in a PyTorch tensor. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the absolute value of a PyTorch tensor element-wise. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the absolute function. + """ + return (torch.abs(tens_a),) diff --git a/modules/pytorch_wrapper/pt_acos.py b/modules/pytorch_wrapper/pt_acos.py new file mode 100644 index 0000000..1d68c2a --- /dev/null +++ b/modules/pytorch_wrapper/pt_acos.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtAcos: + """ + Computes the arccosine (inverse cosine) of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise arccosine (inverse cosine) of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. Values must be in the range [-1, 1]. + + Returns: + tuple: A tuple containing the resultant tensor after applying the arccosine function. + """ + return (torch.acos(tens_a),) diff --git a/modules/pytorch_wrapper/pt_add.py b/modules/pytorch_wrapper/pt_add.py new file mode 100644 index 0000000..4220656 --- /dev/null +++ b/modules/pytorch_wrapper/pt_add.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtAdd: + """ + Adds two PyTorch tensors. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Adds two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports addition with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.add(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_arange.py b/modules/pytorch_wrapper/pt_arange.py new file mode 100644 index 0000000..36a283e --- /dev/null +++ b/modules/pytorch_wrapper/pt_arange.py @@ -0,0 +1,61 @@ +import os +import sys +from typing import Any, Dict, Tuple +import torch +from .utils import DTYPE_MAPPING, str_to_number, str_to_number_with_default + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + + +class PtArange: + """ + Creates a PyTorch tensor using `torch.arange` with the specified start, end, and step values. + + The values are parsed from string inputs and converted to numeric types. This allows users to specify + tensor range values in text fields. The function also ensures proper dtype selection. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "start": ("STRING", {"default":"", "multiline": False}), + "end": ("STRING", {"default":"", "multiline": False}), + "step": ("STRING", {"default":"", "multiline": False}), + "data_type": (("float32", "float16", "bfloat16", "float64", "uint8", "int8", "int16", "int32", "int64"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + + def f(self, start: str, end: str, step: str, data_type: str) -> Tuple[torch.Tensor]: + """ + Creates a PyTorch tensor using `torch.arange` with the given start, stop, and step values. + + Args: + start (str): The starting value of the sequence. If empty, defaults to 0. + end (str): The end value of the sequence (exclusive). + step (str): The step size for generating the sequence. If empty, defaults to 1. + data_type (str): The desired PyTorch dtype for the resulting tensor. + + Returns: + Tuple[torch.Tensor]: A tuple containing the generated PyTorch tensor. + """ + start_value = str_to_number_with_default(start, 0) + end_value = str_to_number(end) + step_value = str_to_number_with_default(step, 1) + tens = torch.arange(start_value, end_value, step_value, dtype=DTYPE_MAPPING[data_type]) + return (tens,) diff --git a/modules/pytorch_wrapper/pt_argmax.py b/modules/pytorch_wrapper/pt_argmax.py new file mode 100644 index 0000000..1e78222 --- /dev/null +++ b/modules/pytorch_wrapper/pt_argmax.py @@ -0,0 +1,59 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtArgmax: + """ + Computes the indices of the maximum values of a PyTorch tensor along the specified dimension(s). + + Specify the dimension in the `dim` field using an integer as shown below: + ``` + 0 + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the indices of the maximum values along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute argmax, + e.g., "0" or "". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the tensor with indices of maximum values. + """ + dim = str_to_dim(dim) + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the argmax along the specified dimension(s) + tens = torch.argmax(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_argmin.py b/modules/pytorch_wrapper/pt_argmin.py new file mode 100644 index 0000000..0f4e30a --- /dev/null +++ b/modules/pytorch_wrapper/pt_argmin.py @@ -0,0 +1,59 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtArgmin: + """ + Computes the indices of the minimum values of a PyTorch tensor along the specified dimension(s). + + Specify the dimension in the `dim` field using an integer as shown below: + ``` + 0 + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the indices of the minimum values along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute argmin, + e.g., "0" or "". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the tensor with indices of minimum values. + """ + dim = str_to_dim(dim) + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the argmin along the specified dimension(s) + tens = torch.argmin(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_asin.py b/modules/pytorch_wrapper/pt_asin.py new file mode 100644 index 0000000..ef9b6dd --- /dev/null +++ b/modules/pytorch_wrapper/pt_asin.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtAsin: + """ + Computes the arcsine (inverse sine) of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise arcsine (inverse sine) of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. Values must be in the range [-1, 1]. + + Returns: + tuple: A tuple containing the resultant tensor after applying the arcsine function. + """ + return (torch.asin(tens_a),) diff --git a/modules/pytorch_wrapper/pt_atan.py b/modules/pytorch_wrapper/pt_atan.py new file mode 100644 index 0000000..93e8c68 --- /dev/null +++ b/modules/pytorch_wrapper/pt_atan.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtAtan: + """ + Computes the arc tangent (inverse tangent) of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise arc tangent (inverse tangent) of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the arc tangent function. + """ + return (torch.atan(tens_a),) diff --git a/modules/pytorch_wrapper/pt_bitwise_and.py b/modules/pytorch_wrapper/pt_bitwise_and.py new file mode 100644 index 0000000..ca130ef --- /dev/null +++ b/modules/pytorch_wrapper/pt_bitwise_and.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtBitwiseAnd: + """ + Performs a bitwise AND operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Bitwise operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a bitwise AND operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of integer type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and integer type as `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor after applying bitwise AND. + """ + return (torch.bitwise_and(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_bitwise_left_shift.py b/modules/pytorch_wrapper/pt_bitwise_left_shift.py new file mode 100644 index 0000000..19cea42 --- /dev/null +++ b/modules/pytorch_wrapper/pt_bitwise_left_shift.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +import torch + +class PtBitwiseLeftShift: + """ + Performs a bitwise left shift operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Bitwise operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a bitwise left shift operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of integer type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and integer type as `tens_a`, + representing the number of positions to shift. + + Returns: + tuple: A tuple containing the resultant tensor after applying bitwise left shift. + """ + return (torch.bitwise_left_shift(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_bitwise_not.py b/modules/pytorch_wrapper/pt_bitwise_not.py new file mode 100644 index 0000000..7fb05c0 --- /dev/null +++ b/modules/pytorch_wrapper/pt_bitwise_not.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtBitwiseNot: + """ + Performs a bitwise NOT operation on a PyTorch tensor element-wise. + + category: PyTorch wrapper - Bitwise operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Performs a bitwise NOT operation on a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of integer type. + + Returns: + tuple: A tuple containing the resultant tensor after applying bitwise NOT. + """ + return (torch.bitwise_not(tens_a),) diff --git a/modules/pytorch_wrapper/pt_bitwise_or.py b/modules/pytorch_wrapper/pt_bitwise_or.py new file mode 100644 index 0000000..b5d86f8 --- /dev/null +++ b/modules/pytorch_wrapper/pt_bitwise_or.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtBitwiseOr: + """ + Performs a bitwise OR operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Bitwise operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a bitwise OR operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of integer type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and integer type as `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor after applying bitwise OR. + """ + return (torch.bitwise_or(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_bitwise_right_shift.py b/modules/pytorch_wrapper/pt_bitwise_right_shift.py new file mode 100644 index 0000000..fafde91 --- /dev/null +++ b/modules/pytorch_wrapper/pt_bitwise_right_shift.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +import torch + +class PtBitwiseRightShift: + """ + Performs a bitwise right shift operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Bitwise operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a bitwise right shift operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of integer type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and integer type as `tens_a`, + representing the number of positions to shift. + + Returns: + tuple: A tuple containing the resultant tensor after applying bitwise right shift. + """ + return (torch.bitwise_right_shift(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_bitwise_xor.py b/modules/pytorch_wrapper/pt_bitwise_xor.py new file mode 100644 index 0000000..bf910e0 --- /dev/null +++ b/modules/pytorch_wrapper/pt_bitwise_xor.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtBitwiseXor: + """ + Performs a bitwise XOR operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Bitwise operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a bitwise XOR operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of integer type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and integer type as `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor after applying bitwise XOR. + """ + return (torch.bitwise_xor(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_bmm.py b/modules/pytorch_wrapper/pt_bmm.py new file mode 100644 index 0000000..9bebbc6 --- /dev/null +++ b/modules/pytorch_wrapper/pt_bmm.py @@ -0,0 +1,44 @@ +from typing import Any, Dict +import torch + +class PtBmm: + """ + Performs batched matrix multiplication on two 3D PyTorch tensors. + + category: PyTorch wrapper - Matrix operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs batched matrix multiplication on two 3D PyTorch tensors. + + Args: + tens_a (torch.Tensor): A 3D PyTorch tensor (batch, m, n). + tens_b (torch.Tensor): A 3D PyTorch tensor (batch, n, p) that supports matrix multiplication with `tens_a`. + + Returns: + tuple: A tuple containing the resultant 3D tensor. + """ + if tens_a.dim() != 3 or tens_b.dim() != 3: + raise ValueError("torch.bmm() only supports batch matrix multiplication for 3D tensors.") + + return (torch.bmm(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_bool_create.py b/modules/pytorch_wrapper/pt_bool_create.py new file mode 100644 index 0000000..016e34a --- /dev/null +++ b/modules/pytorch_wrapper/pt_bool_create.py @@ -0,0 +1,51 @@ +from typing import Any, Dict +import torch +import ast + + +class PtBoolCreate: + """ + Creates a PyTorch tensor of dtype bool from True or False values entered as a list in the text field. + For example, entering: + ``` + [True, False, False] + ``` + will create a 1D boolean tensor. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "data": ("STRING", {"multiline": True}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, data: str) -> tuple: + """ + Creates a PyTorch tensor with bool data type using True or False string values entered in the text field. + + Args: + data (str): String value in the text field. + + Returns: + tuple: A tuple containing the tensor. + """ + # Convert string to Python list + list_data = ast.literal_eval(data) + + # Convert list to PyTorch tensor + tensor = torch.tensor(list_data, dtype=torch.bool) + return (tensor,) diff --git a/modules/pytorch_wrapper/pt_cos.py b/modules/pytorch_wrapper/pt_cos.py new file mode 100644 index 0000000..7a66956 --- /dev/null +++ b/modules/pytorch_wrapper/pt_cos.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtCos: + """ + Computes the cosine of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise cosine of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type, representing angles in radians. + + Returns: + tuple: A tuple containing the resultant tensor after applying the cosine function. + """ + return (torch.cos(tens_a),) diff --git a/modules/pytorch_wrapper/pt_cosh.py b/modules/pytorch_wrapper/pt_cosh.py new file mode 100644 index 0000000..b168937 --- /dev/null +++ b/modules/pytorch_wrapper/pt_cosh.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtCosh: + """ + Computes the hyperbolic cosine of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise hyperbolic cosine of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the hyperbolic cosine function. + """ + return (torch.cosh(tens_a),) diff --git a/modules/pytorch_wrapper/pt_crop.py b/modules/pytorch_wrapper/pt_crop.py new file mode 100644 index 0000000..45c1fc3 --- /dev/null +++ b/modules/pytorch_wrapper/pt_crop.py @@ -0,0 +1,65 @@ +from typing import Any, Dict +import torch + +class PtCrop: + """ + Crops a PyTorch tensor to the specified size. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + + In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + + This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "height": ("INT", {"default":0, "min":1, "max": 16384}), + "width": ("INT", {"default":0, "min":1, "max": 16384}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, height: int, width: int) -> tuple: + """ + Crops a PyTorch tensor. + + Args: + tens (torch.Tensor): A PyTorch tensor with the shape of (c, h, w) or (b, c, h, w). + height (int): New image height. + width (int): New image width. + + Returns: + tuple: A tuple containing a transformed tensor`. + """ + dim = tens.dim() + if dim == 3: # c, h, w + image_height = tens.size(1) + image_width = tens.size(2) + elif dim == 4: # b, c, h, w + image_height = tens.size(2) + image_width = tens.size(3) + else: + raise ValueError("Only rank 3 or 4 tensors are supported for crop.") + + if height > image_height or width > image_width: + raise ValueError("Specified crop size exceeds tensor dimensions.") + + # Compute starting position for cropping + top = max(int((height - image_height) / 2), 0) + left = max(int((width - image_width) / 2), 0) + + tens2 = tens[..., top:top+height, left:left+width] + return (tens2,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_data_loader.py b/modules/pytorch_wrapper/pt_data_loader.py new file mode 100644 index 0000000..7ec163b --- /dev/null +++ b/modules/pytorch_wrapper/pt_data_loader.py @@ -0,0 +1,81 @@ +import inspect +from typing import Any, Dict +import ast +import torch +from torch.utils.data import DataLoader + + +class PtDataLoader: + """ + Loads data from a dataset node and creates a PyTorch DataLoader. + + **dataset** + Connect a PtvDataset node that provides the dataset. + + **batch size** + Specifies the number of samples per batch. + + **shuffle** + If True, shuffles the dataset before each epoch. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types required for data loading. + + Returns: + Dict[str, Any]: A dictionary specifying required and optional input types. + """ + return { + "required": { + "dataset": ("PTVDATASET", {}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 1e6}), + "shuffle": ("BOOLEAN", {"default": False}), + }, + "optional": { + "parameters": ("STRING", {"default": "", "multiline": True}) + } + } + + RETURN_TYPES: tuple = ("PTDATALOADER",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + dataset: Any, # One of the classes defined in torchvision.datasets + batch_size: int, + shuffle: bool, + parameters: str) -> tuple: + """ + Creates a PyTorch DataLoader for the given dataset. + + Args: + dataset (torchvision.datasets.{MNIST, ...}): An instance of a dataset from torchvision.datasets. + batch_size (int): The number of samples per batch. + shuffle (bool): If True, shuffles the dataset before each epoch. + parameters (str): Additional parameters in Python dictionary format. + + Returns: + tuple: A tuple containing the created DataLoader instance. + """ + with torch.inference_mode(False): + param_dict = ast.literal_eval(parameters) + + # Get valid parameters for the DataLoader class + valid_params = inspect.signature(DataLoader).parameters + + # Filter only parameters that match the dataset's constructor + filtered_params = {k: v for k, v in param_dict.items() if k in valid_params} + + dl = DataLoader(dataset=dataset, + batch_size=batch_size, + shuffle=shuffle, + **filtered_params) + return (dl,) diff --git a/modules/pytorch_wrapper/pt_div.py b/modules/pytorch_wrapper/pt_div.py new file mode 100644 index 0000000..db87bc0 --- /dev/null +++ b/modules/pytorch_wrapper/pt_div.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtDiv: + """ + Divides one PyTorch tensor by another element-wise. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Divides one PyTorch tensor by another element-wise. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports element-wise division with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.div(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_einsum.py b/modules/pytorch_wrapper/pt_einsum.py new file mode 100644 index 0000000..ddd4dc8 --- /dev/null +++ b/modules/pytorch_wrapper/pt_einsum.py @@ -0,0 +1,67 @@ +from typing import Any, Dict +import torch + + +class PtEinsum: + """ + Performs Tensor operations specified in the Einstein summation equation. + + Specify Einstein summation notation in the first socket. + Specify two tensors in tens_a, tens_b. If you specify the third tensor, specify in tens_c. + Otherwise, leave the socket unconnected. + + Sublist format is not supported. + + category: PyTorch wrapper - Matrix operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "equation": ("STRING", {"default": ""}), + "tens_a": ("TENSOR", {}) + }, + "optional":{ + "tens_b": ("TENSOR", {}), + "tens_c": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, + equation: str, + tens_a: torch.Tensor, + tens_b: torch.Tensor = None, + tens_c: torch.Tensor = None) -> tuple: + """ + Performs Tensor operations specified in the Einstein summation equation. + + Args: + equation (str): Einstein summation equation. + tens_a (torch.Tensor): A PyTorch tensor. + tens_b (torch.Tensor): A PyTorch tensor. + tens_c (torch.Tensor): A PyTorch tensor. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + if tens_a is None: + raise ValueError("tens_a is None") + + if tens_b is None and tens_c is None: + tens_ret_val = torch.einsum(equation, tens_a) + elif tens_c is None: + tens_ret_val = torch.einsum(equation, tens_a, tens_b) + else: + tens_ret_val = torch.einsum(equation, tens_a, tens_b, tens_c) + return (tens_ret_val,) diff --git a/modules/pytorch_wrapper/pt_eq.py b/modules/pytorch_wrapper/pt_eq.py new file mode 100644 index 0000000..ffadbce --- /dev/null +++ b/modules/pytorch_wrapper/pt_eq.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtEq: + """ + Tests whether two PyTorch tensors are equal element-wise. + + category: PyTorch wrapper - Comparison operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Compares two PyTorch tensors for element-wise equality. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of the same shape as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor indicating element-wise equality. + """ + return (torch.eq(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_evaluate_classification_model.py b/modules/pytorch_wrapper/pt_evaluate_classification_model.py new file mode 100644 index 0000000..f915c32 --- /dev/null +++ b/modules/pytorch_wrapper/pt_evaluate_classification_model.py @@ -0,0 +1,71 @@ +from typing import Any, Dict, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score + +class PtEvaluateClassificationModel: + """ + Performs inference on test data and computes evaluation metrics. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "data_loader": ("PTDATALOADER", {}), + "use_gpu": ("BOOLEAN", {"default":False}) + } + } + + RETURN_NAMES: Tuple[str, ...] = ("accuracy", "precision", "recall", "f1", "num_samples") + RETURN_TYPES: Tuple[str, ...] = ("FLOAT", "FLOAT", "FLOAT", "FLOAT", "INT") + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self, model: nn.Module, data_loader: torch.utils.data.DataLoader, use_gpu: bool) -> Tuple[float, float, float, float, int]: + """ + Performs inference on test data and computes evaluation metrics. + + Args: + model (torch.nn.Module): The PyTorch model to evaluate. + data_loader (torch.utils.data.DataLoader): DataLoader for the test dataset. + use_gpu (bool): Whether to use GPU for inference. + + Returns: + Tuple[float, float, float, float, int]: A tuple containing accuracy, precision, recall, + F1-score, and the number of samples. + """ + + with torch.inference_mode(): + if use_gpu: + model.to("cuda") + model.train(False) # TODO: Change to model.*e*v*a*l() once Comfy's security checker is fixed. + y_hat = [] + y = [] + + for images, labels in data_loader: + if use_gpu: + images = images.to("cuda") + labels = labels.to("cuda") + outputs = model(images) + preds = outputs.argmax(dim=1) + + y_hat.extend(preds.cpu().numpy()) + y.extend(labels.cpu().numpy()) + + accuracy = accuracy_score(y, y_hat) + precision = precision_score(y, y_hat, average='macro') + recall = recall_score(y, y_hat, average='macro') + f1 = f1_score(y, y_hat, average='macro') + num_samples = len(y) + return (accuracy, precision, recall, f1, num_samples) diff --git a/modules/pytorch_wrapper/pt_exp.py b/modules/pytorch_wrapper/pt_exp.py new file mode 100644 index 0000000..3773009 --- /dev/null +++ b/modules/pytorch_wrapper/pt_exp.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtExp: + """ + Performs an exponential operation on a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise exponential of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the exponential function. + """ + return (torch.exp(tens_a),) diff --git a/modules/pytorch_wrapper/pt_flatten.py b/modules/pytorch_wrapper/pt_flatten.py new file mode 100644 index 0000000..ac56d3a --- /dev/null +++ b/modules/pytorch_wrapper/pt_flatten.py @@ -0,0 +1,40 @@ +from typing import Any, Dict +import torch + + +class PtFlatten: + """ + Flattens a PyTorch tensor into a 1D tensor. + + category: PyTorch wrapper - Transform + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Flattens a PyTorch tensor into a 1D tensor. + + Args: + tens (torch.Tensor): A PyTorch tensor of any shape. + + Returns: + tuple: A tuple containing the flattened tensor. + """ + return (tens.flatten(),) diff --git a/modules/pytorch_wrapper/pt_float_create.py b/modules/pytorch_wrapper/pt_float_create.py new file mode 100644 index 0000000..264b986 --- /dev/null +++ b/modules/pytorch_wrapper/pt_float_create.py @@ -0,0 +1,47 @@ +from typing import Any, Dict +import torch +import ast + + +class PtFloatCreate: + """ + Creates a PyTorch tensor with 32-bit floating point precision + using values entered in the text field. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "data": ("STRING", {"multiline": True}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, data: str) -> tuple: + """ + Creates a PyTorch tensor using values entered in the text field. + + Args: + data (str): String value in the text field. + + Returns: + tuple: A tuple containing the tensor. + """ + # Convert string to Python list + list_data = ast.literal_eval(data) + + # Convert list to PyTorch tensor + tensor = torch.tensor(list_data, dtype=torch.float32) + return (tensor,) diff --git a/modules/pytorch_wrapper/pt_floor_divide.py b/modules/pytorch_wrapper/pt_floor_divide.py new file mode 100644 index 0000000..f579d98 --- /dev/null +++ b/modules/pytorch_wrapper/pt_floor_divide.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtFloorDiv: + """ + Performs element-wise floor division on two PyTorch tensors. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs element-wise floor division on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports element-wise floor division with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.floor_divide(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_from_image.py b/modules/pytorch_wrapper/pt_from_image.py new file mode 100644 index 0000000..8ee0d5e --- /dev/null +++ b/modules/pytorch_wrapper/pt_from_image.py @@ -0,0 +1,40 @@ +from typing import Any, Dict +import torch + + +class PtFromImage: + """ + Casts an Image tensor as a PyTorch tensor. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "image": ("IMAGE", {}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, image: torch.Tensor) -> tuple: + """ + Casts an Image tensor as a PyTorch tensor. + + Args: + image (torch.Tensor): Image tensor + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + return (image,) diff --git a/modules/pytorch_wrapper/pt_from_image_transpose.py b/modules/pytorch_wrapper/pt_from_image_transpose.py new file mode 100644 index 0000000..4243728 --- /dev/null +++ b/modules/pytorch_wrapper/pt_from_image_transpose.py @@ -0,0 +1,47 @@ +from typing import Any, Dict +import torch + + +class PtFromImageTranspose: + """ + Casts an image tensor to a PyTorch tensor and transposes it from (H, W, C) to (C, H, W). For rank-4 inputs, the batch axis remains unchanged. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "image": ("IMAGE", {}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, image: torch.Tensor) -> tuple: + """ + Casts an Image tensor as a PyTorch tensor. + + Args: + image (torch.Tensor): Image tensor + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + if image.dim() == 3: + tens = image.permute(2, 0, 1) + elif image.dim() == 4: + tens = image.permute(0, 3, 1, 2) + else: + raise ValueError("Only rank 3 or 4 tensors are supported.") + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_from_latent.py b/modules/pytorch_wrapper/pt_from_latent.py new file mode 100644 index 0000000..bfb581b --- /dev/null +++ b/modules/pytorch_wrapper/pt_from_latent.py @@ -0,0 +1,40 @@ +from typing import Any, Dict +import torch + + +class PtFromLatent: + """ + Casts a latent tensor as a PyTorch tensor. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "latent": ("LATENT", {}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, latent: torch.Tensor) -> tuple: + """ + Casts a latent tensor as a PyTorch tensor. + + Args: + latent (torch.Tensor): Latent tensor + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + return (latent["samples"],) diff --git a/modules/pytorch_wrapper/pt_from_numpy.py b/modules/pytorch_wrapper/pt_from_numpy.py new file mode 100644 index 0000000..0c068d8 --- /dev/null +++ b/modules/pytorch_wrapper/pt_from_numpy.py @@ -0,0 +1,42 @@ +from typing import Any, Dict, Tuple +import numpy as np +import torch + + +class PtFromNumpy: + """ + Converts a NumPy ndarray to a PyTorch tensor while preserving its data type. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "array": ("NDARRAY", {}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, array: np.ndarray) -> Tuple[torch.Tensor]: + """ + Converts a NumPy ndarray to a PyTorch tensor while preserving its data type. + + Args: + array (np.ndarray): NumPy ndarray. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + tens = torch.tensor(array, dtype=torch.from_numpy(array).dtype) + return (tens,) diff --git a/modules/pytorch_wrapper/pt_full.py b/modules/pytorch_wrapper/pt_full.py new file mode 100644 index 0000000..8ce80a8 --- /dev/null +++ b/modules/pytorch_wrapper/pt_full.py @@ -0,0 +1,66 @@ +from typing import Any, Dict +import torch +import ast +from .utils import DTYPE_MAPPING + +class PtFull: + """ + Creates a PyTorch tensor filled with a specified value using the size entered in the text field. + For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32 filled with 7, enter: + [2,3,96,32] for size and 7 for value. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "value": ("STRING", {"multiline": False}), + "size": ("STRING", {"multiline": True}), + "data_type": (("float32", "float16", "bfloat16", "float64", "uint8", "int8", "int16", "int32", "int64", "bool"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, value: str, size: str, data_type: str) -> tuple: + """ + Creates a PyTorch tensor filled with a specified value. + + Args: + value (str): String representing the value to fill the tensor. + size (str): String representation of the tensor size. + data_type (str): Desired data type of the tensor. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + # Convert string to Python list + list_size = ast.literal_eval(size) + + # Convert the value to the appropriate type + dtype = DTYPE_MAPPING[data_type] + + if dtype in {torch.float32, torch.float16, torch.bfloat16, torch.float64}: + fill_value = float(value) + elif dtype in {torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64}: + fill_value = int(value) + elif dtype == torch.bool: + fill_value = value.lower() in {"true", "1"} + else: + raise ValueError(f"Unsupported data type: {data_type}") + + # Convert list to PyTorch Size + sz = torch.Size(list_size) + tens = torch.full(sz, fill_value, dtype=dtype) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_gather.py b/modules/pytorch_wrapper/pt_gather.py new file mode 100644 index 0000000..82c301b --- /dev/null +++ b/modules/pytorch_wrapper/pt_gather.py @@ -0,0 +1,94 @@ +from typing import Any, Dict +import torch +import ast + + +class PtGather: + """ + Generates a tensor based on the index tensor using PyTorch's `gather` function. + + Specify the `dim` field using an integer to indicate the axis along which to gather elements. + Each input tensor element is selected based on the index tensor. + + For example, consider the following input tensor: + ``` + [ [10, 20, 30, 40, 50], + [100, 200, 300, 400, 500]] + ``` + + and the corresponding index tensor: + + ``` + [ [0, 4, 3, 2, 0], + [4, 3, 0, 0, 0]] + ``` + + If `dim = 0`, the function scans row-wise, treating each index value as a row index. + If `dim = 1`, it scans column-wise, interpreting each index value as a column index. + + Let's analyze the case where `dim = 1`: + + * Start at row 0, column 0. + * The index at this position is `index[0, 0] = 0`. + * PyTorch selects the value at position 0 from the array `[10, 20, 30, 40, 50]`, which is `10`. + * The result at this stage is: + ``` + [[10, ?, ?, ?, ?] + [?, ?, ?, ?, ?]] + ``` + + * Moving to column 1: `index[0, 1] = 4`. + * The value at index 4 in `[10, 20, 30, 40, 50]` is `50`. + * The updated result is: + ``` + [[10, 50, ?, ?, ?] + [?, ?, ?, ?, ?]] + ``` + + * Repeating this process for all elements constructs the final gathered tensor. + + category: PyTorch wrapper - Indexing and Slicing Operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("INT", {"default": 0, "min": -10, "max": 10}), + "index": ("STRING", {"multiline": True}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: int, index: str) -> tuple: + """ + Gathers elements along the specified dimension using an index tensor. + + Args: + tens (torch.Tensor): The input tensor. + dim (int): The dimension along which to gather values. + index (str): A string representation of the index tensor, provided in list format. + + Returns: + tuple: A tuple containing the gathered tensor. + """ + # Parse the index string to a Python list + index = ast.literal_eval(index) + + # Convert the parsed list to a tensor + index_tensor = torch.tensor(index, dtype=torch.long, device=tens.device) + + # Perform the gather operation + gathered_tens = torch.gather(tens, dim=dim, index=index_tensor) + + return (gathered_tens,) diff --git a/modules/pytorch_wrapper/pt_ge.py b/modules/pytorch_wrapper/pt_ge.py new file mode 100644 index 0000000..9fc7798 --- /dev/null +++ b/modules/pytorch_wrapper/pt_ge.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtGe: + """ + Tests whether elements in the first PyTorch tensor are greater than or equal to the corresponding elements in the second tensor. + + category: PyTorch wrapper - Comparison operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Compares two PyTorch tensors for element-wise greater-than-or-equal-to condition. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of the same shape as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor indicating where `tens_a >= tens_b`. + """ + return (torch.ge(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_gt.py b/modules/pytorch_wrapper/pt_gt.py new file mode 100644 index 0000000..f30d6f5 --- /dev/null +++ b/modules/pytorch_wrapper/pt_gt.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtGt: + """ + Tests whether elements in the first PyTorch tensor are greater than the corresponding elements in the second tensor. + + category: PyTorch wrapper - Comparison operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Compares two PyTorch tensors for element-wise greater-than condition. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of the same shape as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor indicating where `tens_a > tens_b`. + """ + return (torch.gt(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_index_select.py b/modules/pytorch_wrapper/pt_index_select.py new file mode 100644 index 0000000..8903e68 --- /dev/null +++ b/modules/pytorch_wrapper/pt_index_select.py @@ -0,0 +1,78 @@ +from typing import Any, Dict +import torch +import ast + + +class PtIndexSelect: + """ + Extracts elements from the input tensor along a specified dimension using an index tensor. + + `dim` specifies the dimension along which the selection occurs. + * For example, for 2D tensors: + ** If `dim=0`, `index` selects rows. + ** If `dim=1`, `index` selects columns (for 2D tensors). + + `index` is a list of indices that will be converted into a tensor. + + Example: + ``` + Input Tensor: + [[1, 2, 3], + [40, 50, 60], + [700, 800, 900]] + + index = "[0, 2]" + dim = 0 + + Output Tensor: + [[1, 2, 3], + [700, 800, 900]] + ``` + + category: PyTorch wrapper - Indexing and Slicing Operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("INT", {"default": 0, "min": -10, "max": 10}), + "index": ("STRING", {"multiline": True}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: int, index: str) -> tuple: + """ + Extracts elements from the input tensor along the specified dimension. + + Args: + tens (torch.Tensor): The input tensor. + dim (int): The dimension along which elements are selected. + index (str): A string representation of a list of indices, e.g., "[0, 2]". + + Returns: + tuple: A tuple containing the selected tensor. + """ + # Convert the string index to a Python list + index_list = ast.literal_eval(index) + if not isinstance(index_list, list): + raise ValueError("Index must be a list of integers.") + + # Convert the index list to a tensor + index_tensor = torch.tensor(index_list, dtype=torch.long) + + # Perform index selection + result = torch.index_select(tens, dim=dim, index=index_tensor) + + return (result,) diff --git a/modules/pytorch_wrapper/pt_int_create.py b/modules/pytorch_wrapper/pt_int_create.py new file mode 100644 index 0000000..a1bdb9e --- /dev/null +++ b/modules/pytorch_wrapper/pt_int_create.py @@ -0,0 +1,47 @@ +from typing import Any, Dict +import torch +import ast + + +class PtIntCreate: + """ + Creates a PyTorch tensor with 32-bit integer + using values entered in the text field. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "data": ("STRING", {"multiline": True}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, data: str) -> tuple: + """ + Creates a PyTorch tensor using values entered in the text field. + + Args: + data (str): String value in the text field. + + Returns: + tuple: A tuple containing the tensor. + """ + # Convert string to Python list + list_data = ast.literal_eval(data) + + # Convert list to PyTorch tensor + tensor = torch.tensor(list_data, dtype=torch.int32) + return (tensor,) diff --git a/modules/pytorch_wrapper/pt_interpolate_by_scale_factor.py b/modules/pytorch_wrapper/pt_interpolate_by_scale_factor.py new file mode 100644 index 0000000..f1a5dc7 --- /dev/null +++ b/modules/pytorch_wrapper/pt_interpolate_by_scale_factor.py @@ -0,0 +1,56 @@ +from typing import Any, Dict +import torch + +class PtInterpolateByScaleFactor: + """ + Resizes a PyTorch tensor using interpolation by scale factor. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + + In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + + This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "scale_factor": ("FLOAT", {"default":2.0, "min":0.01, "max": 100.0, "step":0.01}), + "mode": (("nearest-exact", "bilinear", "bicubic", "nearest"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, scale_factor: float, mode: str) -> tuple: + """ + Resizes a PyTorch tensor using interpolation. + + Args: + tens (torch.Tensor): A PyTorch tensor with the shape of (c, h, w) or (b, c, h, w). + scale_factor (float): Scale factor. + mode (string): Interpolation mode. + + Returns: + tuple: A tuple containing a transformed tensor`. + """ + if tens.dim() == 3: + rank3 = True + tens_adjusted = torch.unsqueeze(tens, 0) + else: + rank3 = False + tens_adjusted = tens + tens2 = torch.nn.functional.interpolate(tens_adjusted, scale_factor=scale_factor, mode=mode) + if rank3: + tens2 = torch.squeeze(tens2, 0) + return (tens2,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_interpolate_to_size.py b/modules/pytorch_wrapper/pt_interpolate_to_size.py new file mode 100644 index 0000000..b2183da --- /dev/null +++ b/modules/pytorch_wrapper/pt_interpolate_to_size.py @@ -0,0 +1,60 @@ +from typing import Any, Dict +import torch + +class PtInterpolateToSize: + """ + Resizes a PyTorch tensor using interpolation. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + + In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + + This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "height": ("INT", {"default":0, "min":1, "max": 16384}), + "width": ("INT", {"default":0, "min":1, "max": 16384}), + "mode": (("nearest-exact", "bilinear", "bicubic", "nearest"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, height: int, width: int, mode: str) -> tuple: + """ + Resizes a PyTorch tensor using interpolation. + + Args: + tens (torch.Tensor): A PyTorch tensor with the shape of (c, h, w) or (b, c, h, w). + height (int): New image height. + width (int): New image width. + mode (string): Interpolation mode. + + Returns: + tuple: A tuple containing a transformed tensor`. + """ + if tens.dim() == 3: + rank3 = True + tens_adjusted = torch.unsqueeze(tens, 0) + else: + rank3 = False + tens_adjusted = tens + + tens2 = torch.nn.functional.interpolate(tens_adjusted, size=(height, width), mode=mode) + + if rank3: + tens2 = torch.squeeze(tens2, 0) + return (tens2,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_le.py b/modules/pytorch_wrapper/pt_le.py new file mode 100644 index 0000000..641cf79 --- /dev/null +++ b/modules/pytorch_wrapper/pt_le.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtLe: + """ + Tests whether elements in the first PyTorch tensor are less than or equal to the corresponding elements in the second tensor. + + category: PyTorch wrapper - Comparison operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Compares two PyTorch tensors for element-wise less-than-or-equal-to condition. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of the same shape as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor indicating where `tens_a <= tens_b`. + """ + return (torch.le(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_linspace.py b/modules/pytorch_wrapper/pt_linspace.py new file mode 100644 index 0000000..09ff877 --- /dev/null +++ b/modules/pytorch_wrapper/pt_linspace.py @@ -0,0 +1,61 @@ +import os +import sys +from typing import Any, Dict, Tuple +import torch +from .utils import DTYPE_MAPPING + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + + +class PtLinspace: + """ + Creates a PyTorch tensor using `torch.linspace` with the specified start, end, and steps values. + + The start and end values are parsed from string inputs and converted to a float. + The steps value is converted to an int. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "start": ("STRING", {"default":"", "multiline": False}), + "end": ("STRING", {"default":"", "multiline": False}), + "steps": ("STRING", {"default":"", "multiline": False}), + "data_type": (("float32", "float16", "bfloat16", "float64", "uint8", "int8", "int16", "int32", "int64"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + + def f(self, start: str, end: str, steps: str, data_type: str) -> Tuple[torch.Tensor]: + """ + Creates a PyTorch tensor using `torch.arange` with the given start, end, and step values. + + Args: + start (str): The starting value of the sequence. + end (str): The end value of the sequence (inclusive). + steps (str): The number of steps + data_type (str): The desired PyTorch dtype for the resulting tensor. + + Returns: + Tuple[torch.Tensor]: A tuple containing the generated PyTorch tensor. + """ + start_value = float(start) + end_value = float(end) + steps_value = int(steps) + tens = torch.linspace(start_value, end_value, steps_value, dtype=DTYPE_MAPPING[data_type]) + return (tens,) diff --git a/modules/pytorch_wrapper/pt_load_model.py b/modules/pytorch_wrapper/pt_load_model.py new file mode 100644 index 0000000..eb65601 --- /dev/null +++ b/modules/pytorch_wrapper/pt_load_model.py @@ -0,0 +1,41 @@ +import os +from typing import Any, Dict +import torch +from .utils import get_model_full_path + +class PtLoadModel: + """ + A wrapper class for saving a PyTorch model. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "model_path": ("STRING", {"default": "model.pkl"}) + } + } + + RETURN_NAMES: tuple = ("model",) + RETURN_TYPES: tuple = ("PTMODEL",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, model:Any, model_path: str): + model_full_path = get_model_full_path(model_path) + sd = torch.load(model_full_path, weights_only=True) + model.load_state_dict(sd) + return (model,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_log.py b/modules/pytorch_wrapper/pt_log.py new file mode 100644 index 0000000..bebd1d7 --- /dev/null +++ b/modules/pytorch_wrapper/pt_log.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtLog: + """ + Computes the natural logarithm (log base e) of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise natural logarithm (log base e) of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. Must contain only positive values. + + Returns: + tuple: A tuple containing the resultant tensor after applying the natural logarithm. + """ + return (torch.log(tens_a),) diff --git a/modules/pytorch_wrapper/pt_logical_and.py b/modules/pytorch_wrapper/pt_logical_and.py new file mode 100644 index 0000000..0b19d48 --- /dev/null +++ b/modules/pytorch_wrapper/pt_logical_and.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtLogicalAnd: + """ + Performs a logical AND operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Logical operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a logical AND operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of boolean or numerical type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and type as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor where `tens_a AND tens_b` is applied element-wise. + """ + return (torch.logical_and(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_logical_not.py b/modules/pytorch_wrapper/pt_logical_not.py new file mode 100644 index 0000000..60c83dd --- /dev/null +++ b/modules/pytorch_wrapper/pt_logical_not.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtLogicalNot: + """ + Performs a logical NOT operation on a PyTorch tensor element-wise. + + category: PyTorch wrapper - Logical operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Performs a logical NOT operation on a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of boolean or numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying logical NOT. + """ + return (torch.logical_not(tens_a),) diff --git a/modules/pytorch_wrapper/pt_logical_or.py b/modules/pytorch_wrapper/pt_logical_or.py new file mode 100644 index 0000000..8a6d1f7 --- /dev/null +++ b/modules/pytorch_wrapper/pt_logical_or.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtLogicalOr: + """ + Performs a logical OR operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Logical operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a logical OR operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of boolean or numerical type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and type as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor where `tens_a OR tens_b` is applied element-wise. + """ + return (torch.logical_or(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_logical_xor.py b/modules/pytorch_wrapper/pt_logical_xor.py new file mode 100644 index 0000000..4b162cb --- /dev/null +++ b/modules/pytorch_wrapper/pt_logical_xor.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtLogicalXor: + """ + Performs a logical XOR operation on two PyTorch tensors element-wise. + + category: PyTorch wrapper - Logical operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs a logical XOR operation on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of boolean or numerical type. + tens_b (torch.Tensor): A PyTorch tensor of the same shape and type as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor where `tens_a XOR tens_b` is applied element-wise. + """ + return (torch.logical_xor(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_lt.py b/modules/pytorch_wrapper/pt_lt.py new file mode 100644 index 0000000..2fafb48 --- /dev/null +++ b/modules/pytorch_wrapper/pt_lt.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtLt: + """ + Tests whether elements in the first PyTorch tensor are less than the corresponding elements in the second tensor. + + category: PyTorch wrapper - Comparison operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Compares two PyTorch tensors for element-wise less-than condition. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of the same shape as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor indicating where `tens_a < tens_b`. + """ + return (torch.lt(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_masked_select.py b/modules/pytorch_wrapper/pt_masked_select.py new file mode 100644 index 0000000..3c825e7 --- /dev/null +++ b/modules/pytorch_wrapper/pt_masked_select.py @@ -0,0 +1,62 @@ +from typing import Any, Dict +import torch + + +class PtMaskedSelect: + """ + Extracts elements from the input tensor whose corresponding value in `masked_tens` is `True`. + + Example: + ``` + Input Tensor: + [[1, 2, 3], + [40, 50, 60], + [700, 800, 900]] + + Masked Tensor: + [[True, False, False], + [False, True, False], + [False, False, True]] + + Output Tensor: + [1, 50, 900] + ``` + + category: PyTorch wrapper - Indexing and Slicing Operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "masked_tens": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, masked_tens: torch.Tensor) -> tuple: + """ + Extracts elements from `tens` where `masked_tens` is `True`. + + Args: + tens (torch.Tensor): The input tensor. + masked_tens (torch.Tensor): A boolean tensor specifying which elements to select. + + Returns: + tuple: A tuple containing the selected tensor. + """ + if masked_tens.dtype != torch.bool: + raise ValueError("masked_tens must be a boolean tensor (dtype=torch.bool)") + + result = torch.masked_select(tens, masked_tens) + return (result,) diff --git a/modules/pytorch_wrapper/pt_matmul.py b/modules/pytorch_wrapper/pt_matmul.py new file mode 100644 index 0000000..6401bfe --- /dev/null +++ b/modules/pytorch_wrapper/pt_matmul.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtMatMul: + """ + Performs matrix multiplication on two PyTorch tensors. + + category: PyTorch wrapper - Matrix operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs matrix multiplication on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of shape compatible for matrix multiplication. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports matrix multiplication with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.matmul(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_max.py b/modules/pytorch_wrapper/pt_max.py new file mode 100644 index 0000000..ce3ec28 --- /dev/null +++ b/modules/pytorch_wrapper/pt_max.py @@ -0,0 +1,59 @@ +from typing import Any, Dict +import torch +import ast +from .utils import str_to_dim + +class PtMax: + """ + Computes the maximum values of a PyTorch tensor along the specified dimension(s). + + Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: + ``` + 0, [0], or (1, 2) + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the maximum values along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the maximum, + e.g., "0", "[0]", or "(1, 2)". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the tensor with maximum values. + """ + dim = str_to_dim(dim) + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the max along the specified dimension(s) + tens = torch.amax(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_mean.py b/modules/pytorch_wrapper/pt_mean.py new file mode 100644 index 0000000..1745853 --- /dev/null +++ b/modules/pytorch_wrapper/pt_mean.py @@ -0,0 +1,61 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtMean: + """ + Computes the mean of a PyTorch tensor along the specified dimension(s). + + This node supports only floating-point or complex tensors as input. + + Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: + ``` + 0, [0], or (1, 2) + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the mean along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the mean, + e.g., "0", "[0]", or "(1, 2)". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the mean tensor. + """ + dim = str_to_dim(dim) + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the mean along the specified dimension(s) + tens = torch.mean(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_median.py b/modules/pytorch_wrapper/pt_median.py new file mode 100644 index 0000000..14971e5 --- /dev/null +++ b/modules/pytorch_wrapper/pt_median.py @@ -0,0 +1,66 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtMedian: + """ + Computes the median of a PyTorch tensor along the specified dimension(s). + + Specify the dimension(s) in the `dim` field using an integer as shown below: + ``` + 0 + ``` + or + ``` + 1 + ``` + Note that PtMedian calls torch.median(), which returns the lower of the two numbers when the true median falls between them. + + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the median along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the median, + e.g., "0", or "1". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the median tensor. + """ + dim = str_to_dim(dim) + if not isinstance(dim, int): + raise TypeError(f"Invalid type for dim: {dim}. Must be a integer.") + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the median along the specified dimension(s) + tens, _ = torch.median(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_min.py b/modules/pytorch_wrapper/pt_min.py new file mode 100644 index 0000000..87b3dd5 --- /dev/null +++ b/modules/pytorch_wrapper/pt_min.py @@ -0,0 +1,59 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtMin: + """ + Computes the minimum values of a PyTorch tensor along the specified dimension(s). + + Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: + ``` + 0, [0], or (1, 2) + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the minimum values along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the minimum, + e.g., "0", "[0]", or "(1, 2)". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the tensor with minimum values. + """ + dim = str_to_dim(dim) + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the min along the specified dimension(s) + tens = torch.amin(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_mm.py b/modules/pytorch_wrapper/pt_mm.py new file mode 100644 index 0000000..16f33b1 --- /dev/null +++ b/modules/pytorch_wrapper/pt_mm.py @@ -0,0 +1,44 @@ +from typing import Any, Dict +import torch + +class PtMm: + """ + Performs 2D matrix multiplication on two PyTorch tensors. + + category: PyTorch wrapper - Matrix operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Performs 2D matrix multiplication on two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A 2D PyTorch tensor. + tens_b (torch.Tensor): A 2D PyTorch tensor that supports matrix multiplication with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + if tens_a.dim() != 2 or tens_b.dim() != 2: + raise ValueError("torch.mm() only supports 2D tensors.") + + return (torch.mm(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_mul.py b/modules/pytorch_wrapper/pt_mul.py new file mode 100644 index 0000000..1a4a1c1 --- /dev/null +++ b/modules/pytorch_wrapper/pt_mul.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtMul: + """ + Multiplies two PyTorch tensors element-wise. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Multiplies two PyTorch tensors element-wise. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports element-wise multiplication with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.mul(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_ne.py b/modules/pytorch_wrapper/pt_ne.py new file mode 100644 index 0000000..cd8aab9 --- /dev/null +++ b/modules/pytorch_wrapper/pt_ne.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtNe: + """ + Tests whether two PyTorch tensors are not equal element-wise. + + category: PyTorch wrapper - Comparison operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Compares two PyTorch tensors for element-wise inequality. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of the same shape as `tens_a`. + + Returns: + tuple: A tuple containing a boolean tensor indicating element-wise inequality. + """ + return (torch.ne(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_neg.py b/modules/pytorch_wrapper/pt_neg.py new file mode 100644 index 0000000..c0dd79c --- /dev/null +++ b/modules/pytorch_wrapper/pt_neg.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtNeg: + """ + Computes the negation of each element in a PyTorch tensor. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the negation of a PyTorch tensor element-wise. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying negation. + """ + return (torch.neg(tens_a),) diff --git a/modules/pytorch_wrapper/pt_ones.py b/modules/pytorch_wrapper/pt_ones.py new file mode 100644 index 0000000..7d6131e --- /dev/null +++ b/modules/pytorch_wrapper/pt_ones.py @@ -0,0 +1,53 @@ +from typing import Any, Dict +import torch +import ast +from .utils import DTYPE_MAPPING + +class PtOnes: + """ + Creates a PyTorch tensor of ones using the size entered in the text field. + For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: + [2,3,96,32] + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "size": ("STRING", {"multiline": True}), + "data_type": (("float32", "float16", "bfloat16", "float64", "uint8", "int8", "int16", "int32", "int64", "bool"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, size: str, data_type: str) -> tuple: + """ + Creates a PyTorch tensor of ones using the size entered in the text field. + + Args: + size (str): String value in the text field. + data_type (str): Desired data type of the tensor. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + # Convert string to Python list + list_size = ast.literal_eval(size) + + # Convert list to PyTorch Size + sz = torch.Size(list_size) + + tens = torch.ones(sz, dtype=DTYPE_MAPPING[data_type]) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_pad.py b/modules/pytorch_wrapper/pt_pad.py new file mode 100644 index 0000000..5300460 --- /dev/null +++ b/modules/pytorch_wrapper/pt_pad.py @@ -0,0 +1,72 @@ +from typing import Any, Dict +import torch + +class PtPad: + """ + Pads a PyTorch tensor to the specified size. Padded area will be black. The input tensor must have a shape of (c, h, w) or (b, c, h, w). + + In ComfyUI Analysis, the tensor data type is "TENSOR," while in ComfyUI, the type "IMAGE" is used. To pass image data from ComfyUI to a ComfyUI Analysis node (e.g., this interpolation node), first use the "Pt From Image" node to convert it to a tensor, then transpose the axes from (b, h, w, c) to (b, c, h, w) before passing it to this node. + + This transposition can be performed using the "Pt Permute" node with (0, 2, 3, 1). To convert the output back to an image, apply "Pt Permute" with (0, 3, 1, 2) and then use the "Pt To Image" node. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "height": ("INT", {"default":0, "min":1, "max": 16384}), + "width": ("INT", {"default":0, "min":1, "max": 16384}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, height: int, width: int) -> tuple: + """ + Pads a PyTorch tensor. + + Args: + tens (torch.Tensor): A PyTorch tensor with the shape of (c, h, w) or (b, c, h, w). + height (int): New image height. + width (int): New image width. + + Returns: + tuple: A tuple containing a transformed tensor. + """ + dim = tens.dim() + if dim == 3: # c, h, w + image_height = tens.size(1) + image_width = tens.size(2) + elif dim == 4: # b, c, h, w + image_height = tens.size(2) + image_width = tens.size(3) + else: + raise ValueError("Only rank 3 or 4 tensors are supported for padding.") + + if height < image_height or width < image_width: + raise ValueError("Specified dimensions must be greater than or equal to tensor dimensions.") + + # Compute starting position for padding + top = max((height - image_height) // 2, 0) + left = max((width - image_width) // 2, 0) + + if dim == 3: + tens2 = torch.zeros(tens.size(0), height, width, + dtype=tens.dtype, device=tens.device) + else: + tens2 = torch.zeros(tens.size(0), tens.size(1), height, width, + dtype=tens.dtype, device=tens.device) + + tens2[..., top:top+image_height, left:left+image_width] = tens + return (tens2,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_permute.py b/modules/pytorch_wrapper/pt_permute.py new file mode 100644 index 0000000..86ea0f8 --- /dev/null +++ b/modules/pytorch_wrapper/pt_permute.py @@ -0,0 +1,60 @@ +from typing import Any, Dict +import torch +import ast + + +class PtPermute: + """ + Permutes the dimensions of a PyTorch tensor according to the specified order. + For example, if a tensor has shape (2, 3, 96, 32) and `new_axes="[0, 3, 1, 2]"`, + the output will have shape (2, 32, 3, 96). + + Specify the new axis order in the `new_axes` field using brackets, as shown below: + ``` + [0, 3, 1, 2] + ``` + category: PyTorch wrapper - Transform + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "new_axes": ("STRING", {"multiline": True}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, new_axes: str) -> tuple: + """ + Permutes the dimensions of the input tensor based on the specified order. + + Args: + tens (torch.Tensor): The input tensor. + new_axes (str): A string representation of the desired dimension order, + e.g., "[0, 3, 1, 2]" or "(0, 3, 1, 2)". + + Returns: + tuple: A tuple containing the permuted PyTorch tensor. + """ + # Convert string to a tuple of integers + permute_order = tuple(ast.literal_eval(new_axes)) + + # Ensure the parsed order is valid + if not isinstance(permute_order, tuple) or not all(isinstance(i, int) for i in permute_order): + raise ValueError(f"Invalid format for new_axes: {new_axes}. Must be a tuple or list of integers.") + + # Apply permutation + tens = tens.permute(*permute_order) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_pow.py b/modules/pytorch_wrapper/pt_pow.py new file mode 100644 index 0000000..52f3cb6 --- /dev/null +++ b/modules/pytorch_wrapper/pt_pow.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtPow: + """ + Raises one PyTorch tensor to the power of another element-wise. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Raises one PyTorch tensor to the power of another element-wise. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports element-wise exponentiation with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.pow(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_predict_classification_model.py b/modules/pytorch_wrapper/pt_predict_classification_model.py new file mode 100644 index 0000000..e9e1e70 --- /dev/null +++ b/modules/pytorch_wrapper/pt_predict_classification_model.py @@ -0,0 +1,66 @@ +from typing import Any, Dict, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score + +class PtPredictClassificationModel: + """ + Performs inference on input data. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "inputs": ("TENSOR", {}), + "class_id_to_name_map": ("PYDICT", {}), + "use_gpu": ("BOOLEAN", {"default":False}) + } + } + + RETURN_NAMES: Tuple[str, ...] = ("class_name", "class_id", "probability") + RETURN_TYPES: Tuple[str, ...] = ("STRING", "INT", "FLOAT") + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self, model: nn.Module, + inputs: torch.Tensor, + class_id_to_name_map: Dict, + use_gpu: bool) -> Tuple[str, int, float]: + """ + Performs inference on test data and computes evaluation metrics. + + Args: + model (torch.nn.Module): The PyTorch model to evaluate. + inputs (torch.Tensor): Input image to predict the class for. + class_id_to_name_map (Dict): A Python dictionary contains the mapping of + class ID to class name. + use_gpu (bool): Whether to use GPU for inference. + + Returns: + Tuple[str, int, float]: A tuple containing "class_name", "class_id", "probability". + """ + + with torch.inference_mode(): + if use_gpu: + model.to("cuda") + model.train(False) # TODO: Change to model.*e*v*a*l() once Comfy's security checker is fixed. + x = torch.unsqueeze(inputs, 0) + if use_gpu: + x = x.to("cuda") + outputs = model(x) + class_id = outputs.argmax(dim=1)[0].detach().cpu().item() + prob = F.softmax(outputs, dim=1)[0][class_id].detach().cpu().item() + class_name = class_id_to_name_map[class_id] + + return (class_name, class_id, prob) diff --git a/modules/pytorch_wrapper/pt_prod.py b/modules/pytorch_wrapper/pt_prod.py new file mode 100644 index 0000000..c40316a --- /dev/null +++ b/modules/pytorch_wrapper/pt_prod.py @@ -0,0 +1,65 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtProd: + """ + Computes the product of a PyTorch tensor along the specified dimension(s). + + Specify the dimension(s) in the `dim` field using an integer as shown below: + ``` + 0 + ``` + or + ``` + 1 + ``` + + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the product along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the product, + e.g., "0" or "1". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the product tensor. + """ + dim = str_to_dim(dim) + if not isinstance(dim, int): + raise TypeError(f"Invalid type for dim: {dim}. Must be a integer.") + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the product along the specified dimension(s) + tens = torch.prod(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_rand.py b/modules/pytorch_wrapper/pt_rand.py new file mode 100644 index 0000000..3d967ea --- /dev/null +++ b/modules/pytorch_wrapper/pt_rand.py @@ -0,0 +1,57 @@ +from typing import Any, Dict +import torch +import ast +from .utils import DTYPE_MAPPING + +class PtRand: + """ + Creates a PyTorch tensor with values sampled from a uniform distribution + using the size entered in the text field. + For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: + [2,3,96,32] + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "size": ("STRING", {"multiline": True}), + "data_type": (("float32", "float16", "bfloat16", "float64"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, size: str, data_type: str) -> tuple: + """ + Creates a PyTorch tensor with values sampled from a uniform distribution. + + Args: + size (str): String value in the text field. + data_type (str): Desired floating-point data type of the tensor. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + # Convert string to Python list + list_size = ast.literal_eval(size) + + # Convert list to PyTorch Size + sz = torch.Size(list_size) + tens = torch.rand(sz, dtype=DTYPE_MAPPING[data_type]) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_rand_int.py b/modules/pytorch_wrapper/pt_rand_int.py new file mode 100644 index 0000000..071ea48 --- /dev/null +++ b/modules/pytorch_wrapper/pt_rand_int.py @@ -0,0 +1,72 @@ +import time +from typing import Any, Dict +import torch +import ast +import copy +from .utils import DTYPE_MAPPING + +class PtRandInt: + """ + Creates a PyTorch tensor filled with random integers within a specified range using the size entered in the text field. + For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32 with random integers in the range [0, 10), + enter [2,3,96,32] for size, 0 for min_value, and 10 for max_value. + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "min_value": ("INT", {"default": 0, "min": -2**31, "max": 2**31}), + "max_value": ("INT", {"default": 1, "min": -2**31, "max": 2**31}), + "size": ("STRING", {"multiline": True}), + "data_type": (("uint8", "int8", "int16", "int32", "int64"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def __init__(self): + # Initialize a separate generator with a different seed + self.gen = torch.Generator() + self.gen.manual_seed(int(time.time() * 1000000) % (2**32)) # Unique seed + + def f(self, min_value: int, max_value: int, size: str, data_type: str) -> tuple: + """ + Creates a PyTorch tensor filled with random integers in the range [min_value, max_value). + + Args: + min_value (int): Minimum value (inclusive). + max_value (int): Maximum value (exclusive). + size (str): String representation of the tensor size. + data_type (str): Desired integer data type of the tensor. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + # Convert string to Python list + list_size = ast.literal_eval(size) + + # Convert list to PyTorch Size + sz = torch.Size(list_size) + + # Ensure max_value is greater than min_value + if max_value <= min_value: + raise ValueError("max_value must be greater than min_value.") + + # Generate random integers within the specified range + tens = torch.randint(min_value, max_value, sz, dtype=DTYPE_MAPPING[data_type], generator=self.gen) + return (tens,) diff --git a/modules/pytorch_wrapper/pt_randn.py b/modules/pytorch_wrapper/pt_randn.py new file mode 100644 index 0000000..9084769 --- /dev/null +++ b/modules/pytorch_wrapper/pt_randn.py @@ -0,0 +1,57 @@ +from typing import Any, Dict +import torch +import ast +from .utils import DTYPE_MAPPING + +class PtRandn: + """ + Creates a PyTorch tensor with values sampled from a standard normal distribution (mean=0, std=1) + using the size entered in the text field. + For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: + [2,3,96,32] + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "size": ("STRING", {"multiline": True}), + "data_type": (("float32", "float16", "bfloat16", "float64"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, size: str, data_type: str) -> tuple: + """ + Creates a PyTorch tensor with values sampled from a standard normal distribution. + + Args: + size (str): String value in the text field. + data_type (str): Desired floating-point data type of the tensor. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + # Convert string to Python list + list_size = ast.literal_eval(size) + + # Convert list to PyTorch Size + sz = torch.Size(list_size) + tens = torch.randn(sz, dtype=DTYPE_MAPPING[data_type]) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_remainder.py b/modules/pytorch_wrapper/pt_remainder.py new file mode 100644 index 0000000..25c9591 --- /dev/null +++ b/modules/pytorch_wrapper/pt_remainder.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtRemainder: + """ + Computes the element-wise remainder of division between two PyTorch tensors. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Computes the element-wise remainder of division between two PyTorch tensors. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports element-wise remainder computation with `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.remainder(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_reshape.py b/modules/pytorch_wrapper/pt_reshape.py new file mode 100644 index 0000000..a132f5d --- /dev/null +++ b/modules/pytorch_wrapper/pt_reshape.py @@ -0,0 +1,62 @@ +from typing import Any, Dict +import torch +import ast + + +class PtReshape: + """ + Reshapes a PyTorch tensor into a specified shape using `torch.reshape()`. + The total number of elements must remain unchanged. + + For example, if a tensor has shape (2, 3, 4), and `new_shape="[6, 4]"`, + the output will have shape (6, 4). + + Use -1 to automatically infer a dimension, e.g.: + ``` + [2, -1] + ``` + category: PyTorch wrapper - Transform + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "new_shape": ("STRING", {"multiline": True}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, new_shape: str) -> tuple: + """ + Reshapes the input tensor based on the specified shape. + + Args: + tens (torch.Tensor): The input tensor. + new_shape (str): A string representation of the desired shape, + e.g., "[2, -1]" or "(6, 4)". + + Returns: + tuple: A tuple containing the reshaped PyTorch tensor. + """ + # Convert string to a tuple of integers + shape_order = tuple(ast.literal_eval(new_shape)) + + # Ensure the parsed shape is valid + if not isinstance(shape_order, tuple) or not all(isinstance(i, int) for i in shape_order): + raise ValueError(f"Invalid format for new_shape: {new_shape}. Must be a tuple or list of integers.") + + # Apply reshape operation + tens = tens.reshape(*shape_order) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_save_model.py b/modules/pytorch_wrapper/pt_save_model.py new file mode 100644 index 0000000..993c221 --- /dev/null +++ b/modules/pytorch_wrapper/pt_save_model.py @@ -0,0 +1,36 @@ +import os +from typing import Any, Dict +import torch +from .utils import get_model_full_path + +class PtSaveModel: + """ + A wrapper class for saving a PyTorch model. + + category: IO + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "model_path": ("STRING", {"default": "model.pkl"}) + } + } + + RETURN_NAMES: tuple = ("model full path",) + RETURN_TYPES: tuple = ("STRING",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, model:Any, model_path: str): + model_full_path = get_model_full_path(model_path) + torch.save(model.state_dict(), model_full_path) + return (os.path.realpath(model_full_path),) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_scatter.py b/modules/pytorch_wrapper/pt_scatter.py new file mode 100644 index 0000000..a629089 --- /dev/null +++ b/modules/pytorch_wrapper/pt_scatter.py @@ -0,0 +1,69 @@ +from typing import Any, Dict +import torch +import ast + +class PtScatter: + """ + Generates a new tensor by replacing values at specified positions using an index tensor. + + * Target tensor (tens): Some of whose values are replaced. + * Source tensor (src): Values from this tensor replace a part or whole of the target tensor. + * Index tensor (index): Specifies the positions in the target tensor for replacement. + + category: PyTorch wrapper - Indexing and Slicing Operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("INT", {"default": 0, "min": -10, "max": 10}), + "index": ("STRING", {"multiline": True}), + "src": ("STRING", {"multiline": True}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, + tens: torch.Tensor, + dim: int, + index: str, + src: str) -> tuple: + """ + Replaces elements in the target tensor along the specified dimension using an index tensor. + + Args: + tens (torch.Tensor): The target tensor whose values will be replaced. + dim (int): The dimension along which values are replaced. + index (str): A string representation of the index tensor (list format). + src (str): A string representation of the source tensor (list format). + + Returns: + tuple: A tuple containing the modified tensor. + """ + # Parse the input index and source tensors from string representations + index_list = ast.literal_eval(index) + src_list = ast.literal_eval(src) + + # Convert to tensors + index_tensor = torch.tensor(index_list, dtype=torch.long, device=tens.device) + src_tensor = torch.tensor(src_list, dtype=tens.dtype, device=tens.device) + + # Ensure index and src shapes match + if index_tensor.shape != src_tensor.shape: + raise ValueError(f"Shape mismatch: index tensor {index_tensor.shape} and src tensor {src_tensor.shape} must match.") + + # Perform the scatter operation + updated_tens = torch.scatter(tens.clone(), dim=dim, index=index_tensor, src=src_tensor) + + return (updated_tens,) diff --git a/modules/pytorch_wrapper/pt_show_size.py b/modules/pytorch_wrapper/pt_show_size.py new file mode 100644 index 0000000..d8b154a --- /dev/null +++ b/modules/pytorch_wrapper/pt_show_size.py @@ -0,0 +1,60 @@ +""" +Almost all of this code was taken from +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/py/show_text.py + +See credit/credit.md for the full license. +""" +from typing import List +import torch + + +class PtShowSize: + """ + Displays PyTorch Size object as a string. + + category: Display data + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "size": ("PTSIZE", {}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + FUNCTION = "notify" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (True,) + + CATEGORY = "Data Analysis" + + MAX_NUM_ELEMENTS_TO_DISPLAY = 100 + + def notify(self, size: torch.Size, unique_id=None, extra_pnginfo=None): + text = str(size[0]) + + if unique_id is not None and extra_pnginfo is not None: + if not isinstance(extra_pnginfo, list): + print("Error: extra_pnginfo is not a list") + elif ( + not isinstance(extra_pnginfo[0], dict) + or "workflow" not in extra_pnginfo[0] + ): + print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key") + else: + workflow = extra_pnginfo[0]["workflow"] + node = next( + (x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])), + None, + ) + if node: + node["widgets_values"] = [text] + else: + print("unique_id or extra_pnginfo is None.") + return {"ui": {"text": [text]}, "result": ([text],)} diff --git a/modules/pytorch_wrapper/pt_show_text.py b/modules/pytorch_wrapper/pt_show_text.py new file mode 100644 index 0000000..152db36 --- /dev/null +++ b/modules/pytorch_wrapper/pt_show_text.py @@ -0,0 +1,62 @@ +""" +Almost all of this code was taken from +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/py/show_text.py + +See credit/credit.md for the full license. +""" +from typing import List +import torch + + +class PtShowText: + """ + Displays PyTorch tensor as a string. Note that the tensor is partially printed out when + the tensor contains more elements than the threshold set internally. + + category: Display data + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "tens": ("TENSOR", {}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + FUNCTION = "notify" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (True,) + + CATEGORY = "Data Analysis" + + MAX_NUM_ELEMENTS_TO_DISPLAY = 100 + + def notify(self, tens: torch.Tensor, unique_id=None, extra_pnginfo=None): + torch.set_printoptions(threshold=PtShowText.MAX_NUM_ELEMENTS_TO_DISPLAY) + text = str(tens[0]) + + if unique_id is not None and extra_pnginfo is not None: + if not isinstance(extra_pnginfo, list): + print("Error: extra_pnginfo is not a list") + elif ( + not isinstance(extra_pnginfo[0], dict) + or "workflow" not in extra_pnginfo[0] + ): + print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key") + else: + workflow = extra_pnginfo[0]["workflow"] + node = next( + (x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])), + None, + ) + if node: + node["widgets_values"] = [text] + else: + print("unique_id or extra_pnginfo is None.") + return {"ui": {"text": [text]}, "result": ([text],)} diff --git a/modules/pytorch_wrapper/pt_sin.py b/modules/pytorch_wrapper/pt_sin.py new file mode 100644 index 0000000..fb25085 --- /dev/null +++ b/modules/pytorch_wrapper/pt_sin.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtSin: + """ + Computes the sine of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise sine of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type, representing angles in radians. + + Returns: + tuple: A tuple containing the resultant tensor after applying the sine function. + """ + return (torch.sin(tens_a),) diff --git a/modules/pytorch_wrapper/pt_sinh.py b/modules/pytorch_wrapper/pt_sinh.py new file mode 100644 index 0000000..5707082 --- /dev/null +++ b/modules/pytorch_wrapper/pt_sinh.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtSinh: + """ + Computes the hyperbolic sine of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise hyperbolic sine of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the hyperbolic sine function. + """ + return (torch.sinh(tens_a),) diff --git a/modules/pytorch_wrapper/pt_size.py b/modules/pytorch_wrapper/pt_size.py new file mode 100644 index 0000000..dd3be36 --- /dev/null +++ b/modules/pytorch_wrapper/pt_size.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch +import ast + + +class PtSize: + """ + Extracts the PyTorch Size object of a PyTorch tensor using the size() method. + + category: PyTorch wrapper - Size object support + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR",), + } + } + + RETURN_TYPES: tuple = ("PTSIZE",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Extracts the PyTorch Size object of a PyTorch tensor using the size() method. + + Args: + tens (torch.Tensor): The tensor. + + Returns: + tuple: A tuple containing the PyTorch Size. + """ + return (tens.size(),) diff --git a/modules/pytorch_wrapper/pt_size_create.py b/modules/pytorch_wrapper/pt_size_create.py new file mode 100644 index 0000000..c19b877 --- /dev/null +++ b/modules/pytorch_wrapper/pt_size_create.py @@ -0,0 +1,46 @@ +from typing import Any, Dict +import torch +import ast + + +class PtSizeCreate: + """ + Creates a PyTorch Size using values entered in the text field. + + category: PyTorch wrapper - Size object support + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "data": ("STRING", {"multiline": True}), + } + } + + RETURN_TYPES: tuple = ("PTSIZE",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, data: str) -> tuple: + """ + Creates a PyTorch Size using values entered in the text field. + + Args: + data (str): String value in the text field. + + Returns: + tuple: A tuple containing the PyTorch Size. + """ + # Convert string to Python list + list_data = ast.literal_eval(data) + + # Convert list to PyTorch Size + size = torch.Size(list_data) + return (size,) diff --git a/modules/pytorch_wrapper/pt_size_to_numpy.py b/modules/pytorch_wrapper/pt_size_to_numpy.py new file mode 100644 index 0000000..eb8ce05 --- /dev/null +++ b/modules/pytorch_wrapper/pt_size_to_numpy.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch +import numpy as np + +class PtSizeToNumpy: + """ + Converts PyTorch Size object to NumPy ndarray. + + category: PyTorch wrapper - Size object support + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "sz": ("PTSIZE", {}), + } + } + + RETURN_TYPES: tuple = ("NDARRAY",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, sz: torch.Size) -> tuple: + """ + Converts PyTorch Size object to NumPy ndarray. + + Args: + sz (torch.Size): PyTorch Size + + Returns: + tuple: A tuple containing the ndarray. + """ + array = np.array(sz, dtype=int) + return (array,) diff --git a/modules/pytorch_wrapper/pt_size_to_string.py b/modules/pytorch_wrapper/pt_size_to_string.py new file mode 100644 index 0000000..8dc7992 --- /dev/null +++ b/modules/pytorch_wrapper/pt_size_to_string.py @@ -0,0 +1,40 @@ +from typing import Any, Dict +import torch + + +class PtSizeToString: + """ + Converts PyTorch Size object to a Python string. + + category: PyTorch wrapper - Size object support + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "sz": ("PTSIZE", {}), + } + } + + RETURN_TYPES: tuple = ("STRING",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, sz: torch.Size) -> tuple: + """ + Converts PyTorch Size object to a Python string. + + Args: + sz (torch.Size): PyTorch Size + + Returns: + tuple: A tuple containing the string. + """ + return (str(sz),) diff --git a/modules/pytorch_wrapper/pt_sqrt.py b/modules/pytorch_wrapper/pt_sqrt.py new file mode 100644 index 0000000..b6b4514 --- /dev/null +++ b/modules/pytorch_wrapper/pt_sqrt.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtSqrt: + """ + Computes the square root of each element in a PyTorch tensor. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the square root of a PyTorch tensor element-wise. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the square root function. + """ + return (torch.sqrt(tens_a),) diff --git a/modules/pytorch_wrapper/pt_squeeze.py b/modules/pytorch_wrapper/pt_squeeze.py new file mode 100644 index 0000000..c86f3ce --- /dev/null +++ b/modules/pytorch_wrapper/pt_squeeze.py @@ -0,0 +1,45 @@ +from typing import Any, Dict +import torch +from .utils import DTYPE_MAPPING + +class PtSqueeze: + """ + Removes a dimension at the specified position in the input tensor if it is of size 1. + + Specifying a tuple of dimensions is not supported yet. + + category: PyTorch wrapper - Transform + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("INT", {"default": -1, "min": -1, "max": 10}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: int) -> tuple: + """ + Removes a dimension at the specified position in the input tensor if it is of size 1. + + Args: + tens (torch.Tensor): Input tensor. + dim (int): Dimension to remove if it is of size 1. + + Returns: + tuple: A tuple containing the transformed PyTorch tensor. + """ + tens = torch.squeeze(tens, dim) + return (tens,) diff --git a/modules/pytorch_wrapper/pt_std.py b/modules/pytorch_wrapper/pt_std.py new file mode 100644 index 0000000..d6c78c9 --- /dev/null +++ b/modules/pytorch_wrapper/pt_std.py @@ -0,0 +1,68 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtStd: + """ + Computes the standard deviation of a PyTorch tensor along the specified dimension(s). + + Specify 1 to use Bessel's correction (N-1) for sample standard deviation. + Specify 0 to compute population standard deviation. + + Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: + ``` + 0, [0], or (1, 2) + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "correction": ("INT", {"default": 1, "min": 0, "max": 1}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, correction: int, keepdim: bool) -> tuple: + """ + Computes the standard deviation along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the standard deviation, + e.g., "0", "[0]", or "(1, 2)". + correction (int): 0 for population standard deviation, 1 for sample standard deviation (Bessel's correction). + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the standard deviation tensor. + """ + dim = str_to_dim(dim) + + # Ensure correction is valid (0 or 1) + if correction not in (0, 1): + raise ValueError(f"Invalid correction value: {correction}. Must be 0 (population) or 1 (sample).") + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the standard deviation along the specified dimension(s) + tens = torch.std(tens, dim=dim, correction=correction, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_sub.py b/modules/pytorch_wrapper/pt_sub.py new file mode 100644 index 0000000..026b0b1 --- /dev/null +++ b/modules/pytorch_wrapper/pt_sub.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + +class PtSub: + """ + Subtracts one PyTorch tensor from another. + + category: PyTorch wrapper - Arithmetic operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}), + "tens_b": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor, tens_b: torch.Tensor) -> tuple: + """ + Subtracts one PyTorch tensor from another. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of any shape. + tens_b (torch.Tensor): A PyTorch tensor of shape that supports subtraction from `tens_a`. + + Returns: + tuple: A tuple containing the resultant tensor. + """ + return (torch.sub(tens_a, tens_b),) diff --git a/modules/pytorch_wrapper/pt_sum.py b/modules/pytorch_wrapper/pt_sum.py new file mode 100644 index 0000000..9b43c1e --- /dev/null +++ b/modules/pytorch_wrapper/pt_sum.py @@ -0,0 +1,59 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtSum: + """ + Computes the sum of a PyTorch tensor along the specified dimension(s). + + Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: + ``` + 0, [0], or (1, 2) + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, keepdim: bool) -> tuple: + """ + Computes the sum along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the sum, + e.g., "0", "[0]", or "(1, 2)". + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the summed tensor. + """ + dim = str_to_dim(dim) + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the sum along the specified dimension(s) + tens = torch.sum(tens, dim=dim, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_tan.py b/modules/pytorch_wrapper/pt_tan.py new file mode 100644 index 0000000..0f38135 --- /dev/null +++ b/modules/pytorch_wrapper/pt_tan.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtTan: + """ + Computes the tangent of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise tangent of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type, representing angles in radians. + + Returns: + tuple: A tuple containing the resultant tensor after applying the tangent function. + """ + return (torch.tan(tens_a),) diff --git a/modules/pytorch_wrapper/pt_tanh.py b/modules/pytorch_wrapper/pt_tanh.py new file mode 100644 index 0000000..9e08aca --- /dev/null +++ b/modules/pytorch_wrapper/pt_tanh.py @@ -0,0 +1,39 @@ +from typing import Any, Dict +import torch + +class PtTanh: + """ + Computes the hyperbolic tangent of a PyTorch tensor element-wise. + + category: PyTorch wrapper - Math operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens_a": ("TENSOR", {}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens_a: torch.Tensor) -> tuple: + """ + Computes the element-wise hyperbolic tangent of a PyTorch tensor. + + Args: + tens_a (torch.Tensor): A PyTorch tensor of numerical type. + + Returns: + tuple: A tuple containing the resultant tensor after applying the hyperbolic tangent function. + """ + return (torch.tanh(tens_a),) diff --git a/modules/pytorch_wrapper/pt_to_image.py b/modules/pytorch_wrapper/pt_to_image.py new file mode 100644 index 0000000..cd3ddb4 --- /dev/null +++ b/modules/pytorch_wrapper/pt_to_image.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + + +class PtToImage: + """ + Casts a PyTorch tensor as an Image tensor. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_NAMES: tuple = ("image",) + RETURN_TYPES: tuple = ("IMAGE",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Casts a PyTorch tensor as an Image tensor. + + Args: + tens (torch.Tensor): PyTorch Tensor + + Returns: + tuple: A tuple containing the image tensor. + """ + return (tens,) diff --git a/modules/pytorch_wrapper/pt_to_image_transpose.py b/modules/pytorch_wrapper/pt_to_image_transpose.py new file mode 100644 index 0000000..3218dbc --- /dev/null +++ b/modules/pytorch_wrapper/pt_to_image_transpose.py @@ -0,0 +1,47 @@ +from typing import Any, Dict +import torch + + +class PtToImageTranspose: + """ + Casts a PyTorch tensor as an Image tensor and transposes it from (C, H, W) to (H, W, C). For rank-4 inputs, the batch axis remains unchanged. + + category: PyTorch wrapper - Image processing + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_NAMES: tuple = ("image",) + RETURN_TYPES: tuple = ("IMAGE",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Casts a PyTorch tensor as an Image tensor. + + Args: + tens (torch.Tensor): PyTorch Tensor + + Returns: + tuple: A tuple containing the image tensor. + """ + if tens.dim() == 3: + image = tens.permute(1, 2, 0) + elif tens.dim() == 4: + image = tens.permute(0, 2, 3, 1) + else: + raise ValueError("Only rank 3 or 4 tensors are supported.") + return (image,) diff --git a/modules/pytorch_wrapper/pt_to_latent.py b/modules/pytorch_wrapper/pt_to_latent.py new file mode 100644 index 0000000..b9a7fb2 --- /dev/null +++ b/modules/pytorch_wrapper/pt_to_latent.py @@ -0,0 +1,40 @@ +from typing import Any, Dict +import torch + + +class PtToLatent: + """ + Casts a PyTorch tensor as a latent tensor. + + category: PyTorch wrapper - Tensor data conversion + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_TYPES: tuple = ("LATENT",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Casts a PyTorch tensor as a latent tensor. + + Args: + tens (torch.Tensor): PyTorch Tensor + + Returns: + tuple: A tuple containing the latent tensor. + """ + return ({"samples":tens},) diff --git a/modules/pytorch_wrapper/pt_to_numpy.py b/modules/pytorch_wrapper/pt_to_numpy.py new file mode 100644 index 0000000..4b2b9b7 --- /dev/null +++ b/modules/pytorch_wrapper/pt_to_numpy.py @@ -0,0 +1,41 @@ +from typing import Any, Dict +import torch + + +class PtToNumpy: + """ + Converts PyTorch tensor to NumPy ndarray. + + category: PyTorch wrapper - Tensor data conversion + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_TYPES: tuple = ("NDARRAY",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Converts PyTorch tensor to NumPy ndarray. + + Args: + tens (torch.Tensor): PyTorch Tensor + + Returns: + tuple: A tuple containing the ndarray. + """ + array = tens.cpu().detach().numpy() + return (array,) diff --git a/modules/pytorch_wrapper/pt_to_rgb_tensors.py b/modules/pytorch_wrapper/pt_to_rgb_tensors.py new file mode 100644 index 0000000..a162129 --- /dev/null +++ b/modules/pytorch_wrapper/pt_to_rgb_tensors.py @@ -0,0 +1,50 @@ +from typing import Any, Dict +import torch + + +class PtToRgbTensors: + """ + Splits a PyTorch tensor into R, G, and B tensors. + + category: PyTorch wrapper - Tensor data conversion + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_NAMES: tuple = ("R", "G", "B") + RETURN_TYPES: tuple = ("TENSOR", "TENSOR", "TENSOR") + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Splits a PyTorch tensor into R, G, and B tensors. + + Args: + tens (torch.Tensor): A PyTorch tensor of shape [B, H, W, C] or [H, W, C]. + Assumes the last axis represents the color channels. + + Returns: + tuple: A tuple containing the R, G, and B tensors. + If the input tensor has a batch axis, it will be retained. + + Raises: + ValueError: If the input tensor does not have exactly 3 channels. + """ + if tens.size()[-1] != 3: + raise ValueError(f"Expected a 3-channel tensor, but got {tens.size()[-1]} channels.") + + r, g, b = tens[..., 0], tens[..., 1], tens[..., 2] + return (r, g, b) diff --git a/modules/pytorch_wrapper/pt_train_classification_model.py b/modules/pytorch_wrapper/pt_train_classification_model.py new file mode 100644 index 0000000..1ee0852 --- /dev/null +++ b/modules/pytorch_wrapper/pt_train_classification_model.py @@ -0,0 +1,167 @@ +import os +import sys +from typing import Any, Dict, Optional, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np + +class PtTrainClassificationModel: + """ + Trains a classification model using a given dataset, optimizer, and number of epochs. + + early_stopping_rounds: Specifies the number of rounds to monitor the evaluation loss when early_stopping is enabled and validation data is specified. If the loss does not decrease compared to the current best loss within this period, training will be stopped. + output_best_val_model: Whether to outputs the model with the best val loss or the model from the last epoch. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types required for training the model. + + Returns: + Dict[str, Any]: A dictionary specifying required and optional input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "train_loader": ("PTDATALOADER", {}), + "optimizer": ("PTOPTIMIZER", {}), + "epochs": ("INT", {"default":1, "min":1, "max":1e6}), + "use_gpu": ("BOOLEAN", {"default":False}), + "early_stopping": ("BOOLEAN", {"default":False}), + "early_stopping_rounds": ("INT", {"default":10, "min":1, "max":1000}), + "output_best_val_model": ("BOOLEAN", {"default":True}) + }, + "optional": { + "val_loader": ("PTDATALOADER", {}), + } + } + + RETURN_NAMES: tuple = ("Model", "train loss", "val loss") + RETURN_TYPES: tuple = ("PTMODEL","TENSOR","TENSOR") + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, model: nn.Module, + train_loader: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + epochs: int, + use_gpu: bool, + early_stopping: bool, + early_stopping_rounds: int, + output_best_val_model:bool, + val_loader: Optional[torch.utils.data.DataLoader] = None) -> Tuple[nn.Module, torch.Tensor, torch.Tensor]: + """ + Trains a classification model. + + Args: + model (torch.nn.Module): The PyTorch model to be trained. + train_loader (torch.utils.data.DataLoader): DataLoader containing the training dataset. + optimizer (torch.optim.Optimizer): Optimizer used for training. + epochs (int): Number of training epochs. + use_gpu (bool): Whether to use GPU for training. + early_stopping (bool): Whether to turn on early stopping + early_stopping_rounds (int): Specifies the number of rounds to monitor the evaluation loss when early_stopping is enabled and validation data is specified. If the loss does not decrease compared to the current best loss within this period, training will be stopped. + output_best_val_model (bool): Whether to outputs the model with the best val loss or the model from the last epoch. + val_loader (Optional[torch.utils.data.DataLoader]): DataLoader for validation dataset (optional). + + Returns: + Tuple[torch.nn.Module, torch.Tensor, torch.Tensor]: A tuple containing the trained model, + training loss history, and validation loss history. + """ + + checkpoint_dir = os.path.realpath( + os.path.join(os.path.dirname(__file__), "..", "..", "models", "tmp")) + print(f"Checkpoint tmp directory is set to {checkpoint_dir}") + if os.path.exists(checkpoint_dir) is False: + os.makedirs(checkpoint_dir) + print(f"Created {checkpoint_dir}") + best_checkpoint_path = os.path.join(checkpoint_dir, "best_model.pt") + + best_val_loss = None + rounds_since_best_val_loss = 0 + + with torch.inference_mode(False): + train_loss = [] + val_loss = [] + if use_gpu: + model.to("cuda") + + loss_function = nn.CrossEntropyLoss() # mean loss + + # Train the model + for epoch in range(epochs): + model.train() + total_loss = 0 + total_samples = len(train_loader.dataset) + for images, labels in train_loader: + if use_gpu: + images = images.to("cuda") + labels = labels.to("cuda") + optimizer.zero_grad() + outputs = model(images) + loss = loss_function(outputs, labels) + loss.backward() + optimizer.step() + + batch_size = labels.size(0) + batch_loss = loss.item() * batch_size + total_loss += batch_loss + + epoch_loss = total_loss / total_samples + + print(f"Epoch (train) {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}") + train_loss.append(epoch_loss) + + if val_loader: + with torch.no_grad(): + model.train(False) # TODO: Change to model.*e*v*a*l() once Comfy's security checker is fixed. + total_loss = 0 + total_samples = len(val_loader.dataset) + for images, labels in val_loader: + if use_gpu: + images = images.to("cuda") + labels = labels.to("cuda") + outputs = model(images) + loss = loss_function(outputs, labels) + batch_size = labels.size(0) + batch_loss = loss.item() * batch_size + total_loss += batch_loss + + epoch_loss = total_loss / total_samples + print(f"Epoch (val) {epoch+1}/{epochs}, Loss: {epoch_loss:.4f}") + val_loss.append(epoch_loss) + + if best_val_loss is None or epoch_loss < best_val_loss: + best_val_loss = epoch_loss + rounds_since_best_val_loss = 0 + torch.save(model.state_dict(), best_checkpoint_path) + print(f"Updated best val loss. Saved the model as {best_checkpoint_path}") + else: + rounds_since_best_val_loss += 1 + if early_stopping: + if rounds_since_best_val_loss >= early_stopping_rounds: + print(f"Val loss did not improve in {early_stopping_rounds} rounds. Stopping training.") + break + + if use_gpu: + model.to("cpu") + + if val_loader: + if epoch_loss > best_val_loss and output_best_val_model: + sd = torch.load(best_checkpoint_path, weights_only=True) + model.load_state_dict(sd) + print("Loaded the best model for output") + else: + print("Using the final epoch model for output.") + + return (model, + torch.tensor(train_loss, dtype=torch.float32), + torch.tensor(val_loss, dtype=torch.float32)) \ No newline at end of file diff --git a/modules/pytorch_wrapper/pt_unsqueeze.py b/modules/pytorch_wrapper/pt_unsqueeze.py new file mode 100644 index 0000000..7505b95 --- /dev/null +++ b/modules/pytorch_wrapper/pt_unsqueeze.py @@ -0,0 +1,44 @@ +from typing import Any, Dict +import torch + + +class PtUnsqueeze: + """ + Adds a singleton dimension at the specified position in the input tensor. + For example, if a tensor has shape (3, 4) and `dim=1`, the output will have shape (3, 1, 4). + + category: PyTorch wrapper - Transform + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "dim": ("INT", {"default": -1, "min": -10, "max": 10}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: int) -> tuple: + """ + Adds a singleton dimension at the specified position. + + Args: + tens (torch.Tensor): Input tensor. + dim (int): Dimension at which to insert the new singleton dimension. + + Returns: + tuple: A tuple containing the transformed PyTorch tensor. + """ + tens = torch.unsqueeze(tens, dim) + return (tens,) diff --git a/modules/pytorch_wrapper/pt_var.py b/modules/pytorch_wrapper/pt_var.py new file mode 100644 index 0000000..56829c0 --- /dev/null +++ b/modules/pytorch_wrapper/pt_var.py @@ -0,0 +1,68 @@ +from typing import Any, Dict +import torch +from .utils import str_to_dim + + +class PtVar: + """ + Computes the variance of a PyTorch tensor along the specified dimension(s). + + Specify 1 to use Bessel's correction (N-1) for sample variance. + Specify 0 to compute population variance. + + Specify the dimension(s) in the `dim` field using an integer, a list, or a tuple, as shown below: + ``` + 0, [0], or (1, 2) + ``` + category: PyTorch wrapper - Reduction operation & Summary statistics + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "correction": ("INT", {"default": 1, "min": 0, "max": 1}), + "dim": ("STRING", {"multiline": True}), + "keepdim": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, dim: str, correction: int, keepdim: bool) -> tuple: + """ + Computes the variance along the specified dimension(s). + + Args: + tens (torch.Tensor): The input tensor. + dim (str): A string representation of the dimension(s) along which to compute the variance, + e.g., "0", "[0]", or "(1, 2)". + correction (int): 0 for population variance, 1 for sample variance (Bessel's correction). + keepdim (bool): Whether to retain reduced dimensions. + + Returns: + tuple: A tuple containing the variance tensor. + """ + dim = str_to_dim(dim) + + # Ensure correction is valid (0 or 1) + if correction not in (0, 1): + raise ValueError(f"Invalid correction value: {correction}. Must be 0 (population) or 1 (sample).") + + # Ensure keepdim is a boolean + if not isinstance(keepdim, bool): + raise TypeError(f"Invalid type for keepdim: {keepdim}. Must be a boolean.") + + # Compute the variance along the specified dimension(s) + tens = torch.var(tens, dim=dim, correction=correction, keepdim=keepdim) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_view.py b/modules/pytorch_wrapper/pt_view.py new file mode 100644 index 0000000..3d7de7d --- /dev/null +++ b/modules/pytorch_wrapper/pt_view.py @@ -0,0 +1,62 @@ +from typing import Any, Dict +import torch +import ast + + +class PtView: + """ + Reshapes a PyTorch tensor into a specified shape using `torch.view()`. + The total number of elements must remain unchanged. + + For example, if a tensor has shape (2, 3, 4), and `new_shape="[6, 4]"`, + the output will have shape (6, 4). + + Use -1 to automatically infer a dimension, e.g.: + ``` + [2, -1] + ``` + category: PyTorch wrapper - Transform + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + "new_shape": ("STRING", {"multiline": True}) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor, new_shape: str) -> tuple: + """ + Reshapes the input tensor based on the specified shape. + + Args: + tens (torch.Tensor): The input tensor. + new_shape (str): A string representation of the desired shape, + e.g., "[2, -1]" or "(6, 4)". + + Returns: + tuple: A tuple containing the reshaped PyTorch tensor. + """ + # Convert string to a tuple of integers + shape_order = tuple(ast.literal_eval(new_shape)) + + # Ensure the parsed shape is valid + if not isinstance(shape_order, tuple) or not all(isinstance(i, int) for i in shape_order): + raise ValueError(f"Invalid format for new_shape: {new_shape}. Must be a tuple or list of integers.") + + # Apply view operation + tens = tens.view(*shape_order) + + return (tens,) diff --git a/modules/pytorch_wrapper/pt_where.py b/modules/pytorch_wrapper/pt_where.py new file mode 100644 index 0000000..7635af4 --- /dev/null +++ b/modules/pytorch_wrapper/pt_where.py @@ -0,0 +1,61 @@ +from typing import Any, Dict +import torch + +class PtWhere: + """ + Generates a new tensor by selecting values based on a condition tensor. + + * `condition_tens` (torch.Tensor): Boolean tensor that determines which values are selected. + * `true_tens` (torch.Tensor): Values to use where the condition is `True`. + * `false_tens` (torch.Tensor): Values to use where the condition is `False`. + + category: PyTorch wrapper - Indexing and Slicing Operations + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "condition_tens": ("TENSOR", {}), + "true_tens": ("TENSOR", {}), + "false_tens": ("TENSOR", {}), + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, + condition_tens: torch.Tensor, + true_tens: torch.Tensor, + false_tens: torch.Tensor) -> tuple: + """ + Selects values from `true_tens` or `false_tens` based on `condition_tens`. + + Args: + condition_tens (torch.Tensor): Boolean tensor indicating selection conditions. + true_tens (torch.Tensor): Values used where condition is True. + false_tens (torch.Tensor): Values used where condition is False. + + Returns: + tuple: A tuple containing the modified tensor. + """ + # Ensure condition_tens is a boolean tensor + if condition_tens.dtype != torch.bool: + raise TypeError(f"Expected `condition_tens` to be a boolean tensor, got {condition_tens.dtype}") + + # Ensure all tensors are compatible in shape + if not torch.broadcast_shapes(condition_tens.shape, true_tens.shape, false_tens.shape): + raise ValueError(f"Shape mismatch: condition {condition_tens.shape}, true {true_tens.shape}, false {false_tens.shape}") + + # Perform the conditional selection + tens_out = torch.where(condition_tens, true_tens, false_tens) + + return (tens_out,) diff --git a/modules/pytorch_wrapper/pt_zeros.py b/modules/pytorch_wrapper/pt_zeros.py new file mode 100644 index 0000000..0eeacc0 --- /dev/null +++ b/modules/pytorch_wrapper/pt_zeros.py @@ -0,0 +1,52 @@ +from typing import Any, Dict +import torch +import ast +from .utils import DTYPE_MAPPING + +class PtZeros: + """ + Creates a PyTorch tensor of zeros using the size entered in the text field. + For example, if you want to create a 4D tensor of batch size=2, channel=3, height=96, width=32, enter: + [2,3,96,32] + + category: PyTorch wrapper - Tensor creation + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "size": ("STRING", {"multiline": True}), + "data_type": (("float32", "float16", "bfloat16", "float64", "uint8", "int8", "int16", "int32", "int64", "bool"),) + } + } + + RETURN_TYPES: tuple = ("TENSOR",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, size: str, data_type: str) -> tuple: + """ + Creates a PyTorch tensor of zeros using the size entered in the text field. + + Args: + size (str): String value in the text field. + data_type (str): Desired data type of the tensor. + + Returns: + tuple: A tuple containing the PyTorch tensor. + """ + # Convert string to Python list + list_size = ast.literal_eval(size) + + # Convert list to PyTorch Size + sz = torch.Size(list_size) + tens = torch.zeros(sz, dtype=DTYPE_MAPPING[data_type]) + + return (tens,) diff --git a/modules/pytorch_wrapper/ptn_conv_model.py b/modules/pytorch_wrapper/ptn_conv_model.py new file mode 100644 index 0000000..a31fe36 --- /dev/null +++ b/modules/pytorch_wrapper/ptn_conv_model.py @@ -0,0 +1,99 @@ +from typing import Any, Dict, Tuple +import ast +import torch +from .ptn_conv_model_def import ConvModel + + +class PtnConvModel: + """ + A convolutional model consisting of multiple convolutional layers. + + ### Fields: + - `input_dim`: A string representing the input dimensions in the format "(C,H,W)". + Example: `"(3,28,28)"` for a 3-channel 28x28 image. + + - `penultimate_dim`: An integer specifying the number of features before the output layer. If you specify 0, the number is computed internally. + Default: `0` + + - `output_dim`: An integer specifying the number of output features, which should match the number of target classes in classification. + Default: `10`, Min: `1`, Max: `1e6`. + + - `channel_list`: A string representing a Python list of integers specifying the number of channels per layer excluding the channel for the input (e.g. 3 for the color image). + Example: `"[32,64,128,256,512]"`. + + - `kernel_size_list`: A string representing a Python list of kernel sizes per layer. + Example: `"[3,3,3,3,1]"`. + + - `padding_list`: A string representing a Python list of padding values per layer. + Example: `"[1,1,1,1,0]"`. + + - `downsample_list`: A string representing a Python list of boolean values indicating whether to downsample at each layer. + Example: `"[True,True,True,True,False]"`. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "input_dim": ("STRING", {"default": "(3,28,28)"}), + "penultimate_dim": ("INT", {"default": 0, "min": 0, "max": 1e6}), + "output_dim": ("INT", {"default": 10, "min": 1, "max": 1e6}), + "channel_list": ("STRING", {"default": "[32,64,128,256,512]"}), + "kernel_size_list": ("STRING", {"default": "[3,3,3,3,1]"}), + "padding_list": ("STRING", {"default": "[1,1,1,1,0]"}), + "downsample_list": ("STRING", {"default": "[True,True,True,True,False]"}) + } + } + + RETURN_TYPES: Tuple[str] = ("PTMODEL",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + input_dim:str, + penultimate_dim: int, + output_dim: int, + channel_list: str, + kernel_size_list: str, + padding_list: str, + downsample_list: str) -> None: + """ + Constructs a convolutional neural network model. + + Args: + TODO:Update + + Returns: + Tuple[nn.Module]: A tuple containing the instantiated PyTorch model. + + Raises: + TODO:Update + """ + input_dim_parsed = tuple(ast.literal_eval(input_dim)) + channel_list_parsed = ast.literal_eval(channel_list) + kernel_size_list_parsed = ast.literal_eval(kernel_size_list) + padding_list_parsed = ast.literal_eval(padding_list) + downsample_list_parsed = ast.literal_eval(downsample_list) + + with torch.inference_mode(False): + model = ConvModel( + input_dim_parsed, + penultimate_dim, + output_dim, + channel_list_parsed, + kernel_size_list_parsed, + padding_list_parsed, + downsample_list_parsed) + return (model,) diff --git a/modules/pytorch_wrapper/ptn_conv_model_def.py b/modules/pytorch_wrapper/ptn_conv_model_def.py new file mode 100644 index 0000000..e453c9c --- /dev/null +++ b/modules/pytorch_wrapper/ptn_conv_model_def.py @@ -0,0 +1,159 @@ +from typing import List, Dict, Any, Tuple +import torch +import torch.nn as nn + + +class ConvSingleLayer(nn.Module): + """A single convolutional layer followed by an optional ReLU activation and optional downsampling. + + Shape is tensor of shape `(batch_size, in_channels, height, width)`. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + kernel_size (int): Size of the convolutional kernel. + padding (int): Padding size for convolution. + downsample (bool): If True, applies a 2x2 MaxPooling layer. + apply_activation (bool, optional): If True, applies ReLU activation. Defaults to True. + + pragma: skip_doc + """ + + @classmethod # TODO: Dummy method to suppress ComfyUI warning. DO NOT REMOVE + def INPUT_TYPES(cls) -> Dict[str, Any]: + return {} + + def __init__(self, + in_channels: int, + out_channels: int, + kernel_size: int, + padding: int, + downsample: bool, + bias:bool = False, + apply_activation: bool = True) -> None: + super().__init__() + self.conv = nn.Conv2d(in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + padding=padding) + self.apply_activation = apply_activation + self.activation = nn.ReLU() + self.downsample = downsample + self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2) + + if self.apply_activation: + torch.nn.init.kaiming_normal_(self.conv.weight, + nonlinearity='relu') + else: + torch.nn.init.xavier_normal_(self.conv.weight) + + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + """Forward pass of the convolutional layer. + + Args: + inputs (torch.Tensor): Input tensor of shape `(batch_size, in_channels, height, width)`. + + Returns: + torch.Tensor: Output tensor after convolution, optional activation, and optional pooling. + """ + x = self.conv(inputs) + if self.downsample: + x = self.maxpool(x) + if self.apply_activation: + x = self.activation(x) + return x + + +class ConvModel(nn.Module): + """A multi-layer convolutional neural network with ReLU activation after each convolutional layer, except for the final dense layer. + + Args: + input_dim (Tuple[int, int, int]): Shape of a single input sample (C, H, W). + penultimate_dim (int): Number of features before the final dense layer. If 0 is specified, this value is automatically computed internally. + output_dim (int): Number of output features. + channel_list (List[int]): List of channel sizes for each layer. This should not include the inputs channel. + Input channel is taken from the first element of input_dim. + kernel_size_list (List[int]): List of kernel sizes for each convolutional layer. + padding_list (List[int]): List of padding values for each convolutional layer. + downsample_list (List[bool]): List indicating whether downsampling should be applied after each layer. + + pragma: skip_doc + """ + + @classmethod # TODO: Dummy method to suppress ComfyUI warning + def INPUT_TYPES(cls) -> Dict[str, Any]: + return {} + + def __init__(self, + input_dim: Tuple[int, int, int], + penultimate_dim: int, + output_dim: int, + channel_list: List[int], + kernel_size_list: List[int], + padding_list: List[int], + downsample_list: List[bool]) -> None: + super().__init__() + + # Validate list lengths + if (len(channel_list) != len(kernel_size_list) or + len(kernel_size_list) != len(padding_list) or + len(padding_list) != len(downsample_list)): + raise ValueError("channel_list, kernel_size_list, padding_list, and downsample_list must have the same length.") + + self.channel_list = [input_dim[0]] + channel_list + + # Initialize convolutional layers + layers = [] + for i in range(len(self.channel_list) - 1): + layers.append( + ConvSingleLayer( + in_channels=self.channel_list[i], + out_channels=self.channel_list[i + 1], + kernel_size=kernel_size_list[i], + padding=padding_list[i], + downsample=downsample_list[i] + ) + ) + + self.conv_layers = nn.ModuleList(layers) + + if penultimate_dim == 0: + # Try to auto compute penultimate_dim by processing the dummy input + with torch.no_grad(): + dummy_input = torch.randn(1, *input_dim) # B, C, H, W + x = dummy_input + for layer in self.conv_layers: + x = layer(x) + checked_penultimate_dim = x.numel() # Number of elements + else: + checked_penultimate_dim = penultimate_dim + + self.final_dense = nn.Linear(checked_penultimate_dim, output_dim, bias=True) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + """Forward pass of the model. + + Args: + inputs (torch.Tensor): Input tensor of shape `(batch_size, C, H, W)`. + + Returns: + torch.Tensor: Output tensor of shape `(batch_size, output_dim)`. + + Raises: + ValueError: If inputs is not a rank 4 tensor. + """ + if inputs.dim() not in [3, 4]: + raise ValueError("Inputs is not a rank 3 or 4 tensor.") + + if inputs.dim() == 3: + x = torch.unsqueeze(inputs, 1) + else: + x = inputs + + for layer in self.conv_layers: + x = layer(x) + + x = torch.flatten(x, start_dim=1) # Flatten except batch-axis + x = self.final_dense(x) + return x diff --git a/modules/pytorch_wrapper/ptn_linear_model.py b/modules/pytorch_wrapper/ptn_linear_model.py new file mode 100644 index 0000000..bd2108e --- /dev/null +++ b/modules/pytorch_wrapper/ptn_linear_model.py @@ -0,0 +1,87 @@ +from typing import Any, Dict, Tuple +import ast +import torch +import torch.nn as nn +from .ptn_linear_model_def import DenseModel + + +class PtnLinearModel: + """ + A linear model consisting of dense layers. + + ### Fields: + - `dim_list`: A string representing a Python list of layer dimensions. + It should contain the input dimension for the first layer and the output dimensions + for all layers. + + Example for a two-layer network: + - Input data: 784 + - Layer 1: 784 → 128 + - Layer 2: 128 → 10 + - Specify `"[784,128,10]"` + + - `bias_list`: A string representing a Python list of bias usage (True/False) per layer. + Example: "[True,False]" + + - `num_layers`: The number of layers in the model. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "dim_list": ("STRING", {"default": "784,10"}), + "bias_list": ("STRING", {"default": "{True}"}), + "num_layers": ("INT", {"default": 1, "min": 1, "max": 2000}), + } + } + + RETURN_TYPES: Tuple[str] = ("PTMODEL",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, dim_list: str, bias_list: str, num_layers: int) -> Tuple[nn.Module]: + """ + Constructs a dense neural network model. + + Args: + dim_list (str): A comma-separated string representing layer dimensions. + bias_list (str): A comma-separated string representing bias usage (True/False) per layer. + num_layers (int): The number of layers in the model. + + Returns: + Tuple[nn.Module]: A tuple containing the instantiated PyTorch model. + + Raises: + ValueError: If `dim_list` length does not match `num_layers + 1`. + ValueError: If `bias_list` length does not match `num_layers`. + """ + dims = ast.literal_eval(dim_list) + biases = ast.literal_eval(bias_list) + + if len(dims) - 1 != num_layers: + raise ValueError( + "dim_list and num_layers do not match. dim_list should contain " + "the input dimension for the first layer and output dimensions for all layers. " + "This list should be one element longer than the number of layers." + ) + + if len(biases) != num_layers: + raise ValueError("bias_list and num_layers do not match.") + + with torch.inference_mode(False): + model = DenseModel(dims, biases, num_layers) + + return (model,) diff --git a/modules/pytorch_wrapper/ptn_linear_model_def.py b/modules/pytorch_wrapper/ptn_linear_model_def.py new file mode 100644 index 0000000..28a10e6 --- /dev/null +++ b/modules/pytorch_wrapper/ptn_linear_model_def.py @@ -0,0 +1,99 @@ +from typing import List, Dict, Any +import torch +import torch.nn as nn + + +class DenseSingleLayer(nn.Module): + """A single dense layer followed by a ReLU activation. + + Args: + in_features (int): Number of input features. + out_features (int): Number of output features. + bias (bool): If True, adds a learnable bias to the layer. + + pragma: skip_doc + """ + + @classmethod # TODO: Dummy method to suppress ComfyUI warning + def INPUT_TYPES(cls) -> Dict[str, Any]: + return { + } + + def __init__(self, in_features: int, out_features: int, bias: bool) -> None: + super().__init__() + self.fc = nn.Linear(in_features, out_features, bias) + self.activation = nn.ReLU() + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + """Forward pass of the dense layer with ReLU activation. + + Args: + inputs (torch.Tensor): Input tensor of shape `(batch_size, in_features)`. + + Returns: + torch.Tensor: Output tensor of shape `(batch_size, out_features)`. + """ + x = self.fc(inputs) + x = self.activation(x) + return x + + +class DenseModel(nn.Module): + """A multi-layer dense neural network with ReLU activation after each dense layer, except for the final layer. + + Args: + dim_list (List[int]): List of feature dimensions. Should be of length `num_layers + 1`. + bias_list (List[bool]): List indicating whether each layer should have a bias. + num_layers (int): Number of layers in the model. + + Example: + For a three-layer network: + - Layer 1: input -> output1 + - Layer 2: output1 -> output2 + - Layer 3: output2 -> output3 + + `dim_list = [input_dim, output1_dim, output2_dim, output3_dim]` + + pragma: skip_doc + """ + + @classmethod # TODO: Dummy method to suppress ComfyUI warning + def INPUT_TYPES(cls) -> Dict[str, Any]: + return { + } + + def __init__(self, dim_list: List[int], bias_list: List[bool], num_layers: int) -> None: + super().__init__() + + self.no_activation_layers = nn.ModuleList([ + DenseSingleLayer(dim_list[i], dim_list[i + 1], bias_list[i]) + for i in range(num_layers - 1) + ]) + + self.dim_list = dim_list + self.bias_list = bias_list + self.num_layers = num_layers + + self.final_dense = nn.Linear( + dim_list[num_layers - 1], # in_features + dim_list[num_layers], # out_features + bias_list[num_layers - 1] + ) + + def forward(self, inputs: torch.Tensor) -> torch.Tensor: + """Forward pass of the dense model. + + Args: + inputs (torch.Tensor): Input tensor of shape `(batch_size, dim_list[0])`. + + Returns: + torch.Tensor: Output tensor of shape `(batch_size, dim_list[-1])`. + """ + x = inputs.view(-1, self.dim_list[0]) # Ensure correct input shape + + for i in range(self.num_layers - 1): + x = self.no_activation_layers[i](x) + + x = self.final_dense(x) + + return x diff --git a/modules/pytorch_wrapper/ptn_resnet_model.py b/modules/pytorch_wrapper/ptn_resnet_model.py new file mode 100644 index 0000000..9638fcb --- /dev/null +++ b/modules/pytorch_wrapper/ptn_resnet_model.py @@ -0,0 +1,84 @@ +from typing import Any, Dict, Tuple +import ast +import torch +from .ptn_resnet_model_def import ResnetModel + + +class PtnResnetModel: + """ + A Resnet model consisting of multiple Resnet layers. + + ### Fields: + - `input_dim`: A string representing the input dimensions in the format "(C,H,W)". + Example: `"(3,32,32)"` for a 3-channel 28x28 image. + + - `output_dim`: An integer specifying the number of output features, which should match the number of target classes in classification. + Default: `10`, Min: `1`, Max: `1e6`. + + - `num_blocks`: Number of Resnet blocks for 64 channel blocks. Same number of blocks will be created for 128 channel and 256 channel blocks. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "input_dim": ("STRING", {"default": "(3,28,28)"}), + "output_dim": ("INT", {"default": 10, "min": 1, "max": 1e6}), + "num_blocks": ("INT", {"default": 2, "min": 1, "max": 100}) + } + } + + RETURN_TYPES: Tuple[str] = ("PTMODEL",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + input_dim:str, + output_dim: int, + num_blocks: int) -> None: + """ + Constructs a Resnet model. + + Args: + input_dim (str): A string representing the input dimensions in the format "(C,H,W)". Example: `"(3,32,32)"` for a 3-channel 28x28 image. + + output_dim (int): An integer specifying the number of output features, which should match the number of target classes in classification. + Default: `10`, Min: `1`, Max: `1e6`. + + num_blocks (int): Number of Resnet blocks for 64 channel blocks. Same number of blocks will be created for 128 channel and 256 channel blocks. + + Returns: + Tuple[nn.Module]: A tuple containing the instantiated PyTorch model. + """ + input_dim_parsed = tuple(ast.literal_eval(input_dim)) + + if len(input_dim_parsed) != 3: + raise ValueError("Input dimension needs to contain (input channels, input height, input width)") + + if num_blocks < 1: + raise ValueError("Num_blocks has to be equal to or greater than 1") + + if output_dim < 1: + raise ValueError("Output_dim has to be equal to or greater than 1") + + with torch.inference_mode(False): + model = ResnetModel( + num_blocks=num_blocks, + in_channels=input_dim_parsed[0], # in_channels + input_height=input_dim_parsed[1], + input_width=input_dim_parsed[2], + output_dim=output_dim) + + return (model,) diff --git a/modules/pytorch_wrapper/ptn_resnet_model_def.py b/modules/pytorch_wrapper/ptn_resnet_model_def.py new file mode 100644 index 0000000..4bb56ca --- /dev/null +++ b/modules/pytorch_wrapper/ptn_resnet_model_def.py @@ -0,0 +1,214 @@ +""" +Based on my earlier work: +https://github.com/hideyukiinada/cifar10/blob/master/project/keras_25 +""" +from typing import List, Dict, Any, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F + +class SingleConvBlock(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, strides): + super().__init__() + padding = kernel_size // 2 + self.conv = nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=strides, + padding=padding, + bias=False) + self.bn = nn.BatchNorm2d(out_channels) + self.relu = nn.ReLU() + + torch.nn.init.kaiming_normal_(self.conv.weight, + nonlinearity='relu') + + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + x = self.relu(x) + return x + + +class SingleConvBlockNoActivation(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size, strides): + super().__init__() + padding = kernel_size // 2 + self.conv = nn.Conv2d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + stride=strides, + padding=padding, + bias=False) + self.bn = nn.BatchNorm2d(out_channels) + + torch.nn.init.xavier_normal_(self.conv.weight) + + def forward(self, x): + x = self.conv(x) + x = self.bn(x) + return x + + +class SingleResnetBlock(nn.Module): + + def __init__(self, in_channels, out_channels, kernel_size=3, downsample=False): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + self.kernel_size = kernel_size + self.downsample = downsample + + if self.downsample: + self.single_conv_k1_s2_na = \ + SingleConvBlockNoActivation( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=1, + strides=2) # Downsample + + self.first_conv_block = \ + SingleConvBlock( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size, + strides=2) + else: + self.first_conv_block = \ + SingleConvBlock( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size, + strides=1) + + if self.in_channels != self.out_channels: + self.single_conv_k1_s1_na = \ + SingleConvBlockNoActivation( + in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=1, + strides=1) # Channel change without downsampling + + self.second_conv = \ + SingleConvBlockNoActivation( + in_channels=self.out_channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size, + strides=1) + + self.relu = nn.ReLU() + + def forward(self, inputs): + original_tens = inputs + tens = inputs + + if self.downsample: + original_tens = self.single_conv_k1_s2_na(original_tens) + elif self.in_channels != self.out_channels: + original_tens = self.single_conv_k1_s1_na(original_tens) + + tens = self.first_conv_block(tens) + + # Second conv + tens = self.second_conv(tens) + tens = tens + original_tens + tens = self.relu(tens) + return tens + + +class NResnetBlocks(nn.Module): + + def __init__( + self, + num_blocks, + in_channels, + out_channels, + kernel_size=3, + downsample=False): + super().__init__() + self.num_blocks = num_blocks + self.in_channels = in_channels + self.out_channels = out_channels + self.kernel_size = kernel_size + self.downsample = downsample + + if self.downsample: + self.single_resnet_block = SingleResnetBlock(in_channels=self.in_channels, + out_channels=self.out_channels, + kernel_size=kernel_size, + downsample=True) + block_count = self.num_blocks - 1 + else: + block_count = self.num_blocks + + resnet_blocks = [] + for i in range(block_count): + if i == 0 and not self.downsample: + channels = self.in_channels + else: + channels = self.out_channels + resnet_blocks.append( + SingleResnetBlock( + in_channels=channels, + out_channels=self.out_channels, + kernel_size=self.kernel_size) + ) + self.resnet_blocks = nn.ModuleList(resnet_blocks) + + def forward(self, inputs): + tens = inputs + if self.downsample: + tens = self.single_resnet_block(tens) + for block in self.resnet_blocks: + tens = block(tens) + + return tens + +class ResnetModel(nn.Module): + def __init__(self, + num_blocks, + in_channels, + input_height, + input_width, + output_dim): + super().__init__() + self.num_blocks = num_blocks + self.in_channels = in_channels + + self.single_conv_block = SingleConvBlock( + in_channels, out_channels=64, kernel_size=3, strides=1) + + self.n_resnet_blocks64 = NResnetBlocks( + num_blocks, in_channels=64, out_channels=64, kernel_size=3) + self.n_resnet_blocks128 = NResnetBlocks( + num_blocks, in_channels=64, out_channels=128, kernel_size=3, downsample=True) + self.n_resnet_blocks256 = NResnetBlocks( + num_blocks, in_channels=128, out_channels=256, kernel_size=3, downsample=True) + + self.gap = nn.AdaptiveAvgPool2d((1, 1)) # Global Average Pooling + + # Compute output size dynamically + with torch.no_grad(): + dummy_input = torch.zeros(1, in_channels, input_height, input_width) + dummy_output = self._forward_features(dummy_input) + feature_dim = dummy_output.shape[1] * dummy_output.shape[2] * dummy_output.shape[3] + + self.linear = nn.Linear(feature_dim, output_dim) # Use computed feature size + + def _forward_features(self, inputs): + """Forward pass to compute feature map dimensions.""" + x = self.single_conv_block(inputs) + x = self.n_resnet_blocks64(x) + x = self.n_resnet_blocks128(x) + x = self.n_resnet_blocks256(x) + return self.gap(x) # Before flattening + + def forward(self, inputs): + tens = self._forward_features(inputs) + tens = torch.flatten(tens, start_dim=1) # Flatten to (batch_size, computed_feature_dim) + tens = self.linear(tens) + return tens + diff --git a/modules/pytorch_wrapper/pto_adam.py b/modules/pytorch_wrapper/pto_adam.py new file mode 100644 index 0000000..ddc1c1a --- /dev/null +++ b/modules/pytorch_wrapper/pto_adam.py @@ -0,0 +1,46 @@ +from typing import Any, Dict +import torch +import torch.nn as nn +import torch.nn.functional as F + +class PtoAdam: + """ + Instantiates the Adam optimizer. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "model": ("PTMODEL", {}), + "learning_rate": ("FLOAT", {"default":0.001, "min":1e-10, "max":1, "step":0.0000000001}), + "beta1": ("FLOAT", {"default":0.9, "min":1e-10, "max":1, "step":0.0000000001}), + "beta2": ("FLOAT", {"default":0.999, "min":1e-10, "max":1, "step":0.0000000001}), + } + } + + RETURN_TYPES: tuple = ("PTOPTIMIZER",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, model, learning_rate, beta1, beta2) -> tuple: + """ + Converts PyTorch tensor to NumPy ndarray. + + Args: + tens (torch.Tensor): PyTorch Tensor + + Returns: + tuple: A tuple containing the ndarray. + """ + with torch.inference_mode(False): + opt = torch.optim.Adam(model.parameters(), lr=learning_rate, betas=(beta1, beta2)) + return(opt,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/ptv_dataset.py b/modules/pytorch_wrapper/ptv_dataset.py new file mode 100644 index 0000000..29984fd --- /dev/null +++ b/modules/pytorch_wrapper/ptv_dataset.py @@ -0,0 +1,124 @@ +import inspect +from typing import Any, Dict, Optional, Callable, Tuple +import torch +import torchvision.datasets as datasets +import ast + +from .utils import get_dataset_full_path + +class PtvDataset: + """ + A Torchvision Dataset class wrapper. + + Parameters + + **name** + Specify a dataset class name such as MNIST, FashionMNIST in string. + This will be converted to a dataset class internally. + + **download** + Set to True if you want to download. + + **root** + Specify the root directory of the downloaded dataset relative to the + dataset directory of this extension, or specify the absolute path. + If you leave as a blank, the dataset will be downloaded under the `datasets` directory + of this extension. + + **transform** + Data transforms e.g. transform to PyTorch tensor, normalize image. + Plug in the PTV Transforms node that contains Torchvision Transforms functionality. + + **parameters** + Specify other parameters in Python dict format. + For example, if you want to set Train parameter to True to download the train set, specify: + ``` + {"train": True} + ``` + or if you want to download the test set, specify: + ``` + {"train": False} + ``` + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "name": ("STRING", {"default": "", "multiline": False}), + "download": ("BOOLEAN", {"default": True}), + "root": ("STRING", {"default": "", "multiline": False}), + }, + "optional": { + "transform": ("PTVTRANSFORM", {}), + "parameters": ("STRING", {"default": "", "multiline": True}) + } + } + + RETURN_TYPES: Tuple[str] = ("PTVDATASET",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + name: str, + download: bool, + root: str, + parameters: str, + transform: Optional[Callable] = None) -> Tuple: + """ + Loads a dataset from Torchvision with specified parameters. + + Args: + name (str): Name of the dataset class (e.g., MNIST, FashionMNIST). + download (bool): Whether to download the dataset. + root (str): Root directory for storing the dataset. + parameters (str): Additional dataset parameters in stringified dictionary format. + transform (Optional[Callable]): Transformations to apply to the dataset. + + Returns: + Tuple: A tuple containing the dataset instance. + """ + + with torch.inference_mode(False): + param_dict = ast.literal_eval(parameters) + + # Converts the datasets class name to dataset class itself + try: + dataset_class = getattr(datasets, name) + except AttributeError: + raise ValueError(f"Dataset '{name}' is not available in torchvision.datasets") + + # Get valid parameters for the dataset class + valid_params = inspect.signature(dataset_class).parameters + + # Filter only parameters that match the dataset's constructor + filtered_params = {k: v for k, v in param_dict.items() if k in valid_params} + + if root: + dataset_path = get_dataset_full_path(root) + else: # Use default directory + dataset_path = get_dataset_full_path("") + + if transform: + dc = dataset_class(download=download, + root=dataset_path, + transform=transform, + **filtered_params) + else: + dc = dataset_class(download=download, + root=dataset_path, + **filtered_params) + + return (dc,) diff --git a/modules/pytorch_wrapper/ptv_dataset_len.py b/modules/pytorch_wrapper/ptv_dataset_len.py new file mode 100644 index 0000000..f81315f --- /dev/null +++ b/modules/pytorch_wrapper/ptv_dataset_len.py @@ -0,0 +1,40 @@ +from typing import Any, Dict + + +class PtvDatasetLen: + """ + A wrapper class that calls Python len on a dataset. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the f function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "dataset": ("PTVDATASET", {}) + } + } + + RETURN_TYPES: tuple = ("INT",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self, dataset: Any) -> tuple: + """ + A wrapper class that calls Python len on a dataset. + + Args: + iterable (Any): A dataset. + + Returns: + tuple: A tuple containing the length of the dataset. + """ + return (len(dataset),) + \ No newline at end of file diff --git a/modules/pytorch_wrapper/ptv_dataset_loader.py b/modules/pytorch_wrapper/ptv_dataset_loader.py new file mode 100644 index 0000000..40ad553 --- /dev/null +++ b/modules/pytorch_wrapper/ptv_dataset_loader.py @@ -0,0 +1,148 @@ +import ast +import inspect +from typing import Any, Dict, Tuple +import torch +import torchvision.datasets as datasets +from torch.utils.data import DataLoader +import torchvision.transforms as transforms +from .utils import get_dataset_full_path + + +class PtvDatasetLoader: + """ + A node to combine the dataset and data loader into a single node. + + Transform is internally called to convert input data to PyTorch tensors. + + Parameters + + **name** + Specify the dataset class name such as MNIST, FashionMNIST in string. + This will be converted to a dataset class internally. + + **download** + Set to True if you want to download. + + **root** + Specify the root directory of the downloaded dataset relative to the + dataset directory of this extension, or specify the absolute path. + If you leave as a blank, the dataset will be downloaded under the `datasets` directory + of this extension. + + **batch_size** + The number of samples per batch. + + **shuffle** + If True, shuffles the dataset before each epoch. + + **dataset parameters** + Specify other parameters in Python dict format. + For example, if you want to set Train parameter to True to download the train set, specify: + ``` + {"train": True} + ``` + or if you want to download the test set, specify: + ``` + {"train": False} + ``` + + **load parameters** + Additional parameters in Python dictionary format. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "name": ("STRING", {"default": "", "multiline": False}), + "download": ("BOOLEAN", {"default": True}), + "root": ("STRING", {"default": "", "multiline": False}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 1e6}), + "shuffle": ("BOOLEAN", {"default": False}), + }, + "optional": { + "dataset_parameters": ("STRING", {"default": '{"train":True}', "multiline": True}), + "load_parameters": ("STRING", {"default": '{"num_workers:1}', "multiline": True}) + } + } + + RETURN_TYPES: Tuple[str] = ("PTDATALOADER",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + name: str, + download: bool, + root: str, + batch_size: int, + shuffle: bool, + dataset_parameters: str, + load_parameters: str) -> Tuple: + """ + Loads a dataset from Torchvision with specified parameters. + + Args: + name (str): Name of the dataset class (e.g., MNIST, FashionMNIST). + download (bool): Whether to download the dataset. + root (str): Root directory for storing the dataset. + batch_size (int): The number of samples per batch. + shuffle (bool): If True, shuffles the dataset before each epoch. + dataset_parameters (str): Additional dataset parameters in stringified dictionary format. + load_parameters (str): Additional parameters for data load in Python dictionary format. + + Returns: + Tuple: A tuple containing the dataset instance. + """ + + with torch.inference_mode(False): + dataset_param_dict = ast.literal_eval(dataset_parameters) + + # Converts the datasets class name to dataset class itself + try: + dataset_class = getattr(datasets, name) + except AttributeError: + raise ValueError(f"Dataset '{name}' is not available in torchvision.datasets") + + # Get valid parameters for the dataset class + valid_params = inspect.signature(dataset_class).parameters + + # Filter only parameters that match the dataset's constructor + filtered_params = {k: v for k, v in dataset_param_dict.items() if k in valid_params} + + if root: + dataset_path = get_dataset_full_path(root) + else: # Use default directory + dataset_path = get_dataset_full_path("") + + transform = transforms.Compose([transforms.ToTensor()]) + dc = dataset_class(download=download, + root=dataset_path, + transform=transform, + **filtered_params) + + + load_param_dict = ast.literal_eval(load_parameters) + + # Get valid parameters for the DataLoader class + valid_params = inspect.signature(DataLoader).parameters + + # Filter only parameters that match the dataset's constructor + filtered_params = {k: v for k, v in load_param_dict.items() if k in valid_params} + + dl = DataLoader(dataset=dc, + batch_size=batch_size, + shuffle=shuffle, + **filtered_params) + return (dl,) \ No newline at end of file diff --git a/modules/pytorch_wrapper/ptv_image_folder_dataset.py b/modules/pytorch_wrapper/ptv_image_folder_dataset.py new file mode 100644 index 0000000..44ac685 --- /dev/null +++ b/modules/pytorch_wrapper/ptv_image_folder_dataset.py @@ -0,0 +1,73 @@ +from typing import Any, Dict, Optional, Callable, Tuple +import torch +from torchvision.datasets import ImageFolder + +from .utils import get_dataset_full_path + +class PtvImageFolderDataset: + """ + A Torchvision ImageFolder Dataset class wrapper. + + Parameters + + **root** + Specify the root directory that contains subdirectories which of which contains images for each class. + The subdirectory name needs to be named after each class. + + **transform** + Data transforms e.g. transform to PyTorch tensor, normalize image. + Plug in the PTV Transforms node that contains Torchvision Transforms functionality. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: A dictionary of required input types. + """ + return { + "required": { + "root": ("STRING", {"default": "", "multiline": False}) + }, + "optional": { + "transform": ("PTVTRANSFORM", {}) + } + } + + RETURN_TYPES: Tuple[str] = ("PTVDATASET",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, + root: str, + transform: Optional[Callable] = None) -> Tuple: + """ + Loads a dataset from Torchvision with specified parameters. + + Args: + root (str): Root directory for storing the dataset. + transform (Optional[Callable]): Transformations to apply to the dataset. + + Returns: + Tuple: A tuple containing the dataset instance. + """ + + with torch.inference_mode(False): + + if root: + dataset_path = get_dataset_full_path(root) + else: # Use default directory + dataset_path = get_dataset_full_path("") + + dc = ImageFolder(root=dataset_path, + transform=transform + ) + return (dc,) diff --git a/modules/pytorch_wrapper/ptv_transforms_resize.py b/modules/pytorch_wrapper/ptv_transforms_resize.py new file mode 100644 index 0000000..39b5eb7 --- /dev/null +++ b/modules/pytorch_wrapper/ptv_transforms_resize.py @@ -0,0 +1,48 @@ +from typing import Any, Dict +import torchvision.transforms as transforms + + +class PtvTransformsResize: + """ + Resizes and transforms elements of dataset to PyTorch tensors. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: An empty dictionary. + """ + return { + "required": { + "height": ("INT", {"default": 256, "min": 1, "max": 32768}), + "width": ("INT", {"default": 256, "min": 1, "max": 32768}) + }, + } + + RETURN_TYPES: tuple = ("PTVTRANSFORM",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self, height: int, width: int) -> tuple: + """ + Resizes and transforms elements of dataset to PyTorch tensors. + + Args: + height (int): Target height of the input image after transform + width (int): Target width of the inpt image after transform + + Returns: + tuple: A tuple containing the transform callable. + """ + c = transforms.Compose([ + transforms.Resize((height, width)), + transforms.ToTensor() + ]) + return(c,) + + diff --git a/modules/pytorch_wrapper/ptv_transforms_to_tensor.py b/modules/pytorch_wrapper/ptv_transforms_to_tensor.py new file mode 100644 index 0000000..89fdded --- /dev/null +++ b/modules/pytorch_wrapper/ptv_transforms_to_tensor.py @@ -0,0 +1,37 @@ +from typing import Any, Dict, Optional, Callable +import torchvision.transforms as transforms + + +class PtvTransformsToTensor: + """ + Transforms elements of dataset to PyTorch tensors. + + category: PyTorch wrapper - Training + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the required input types. + + Returns: + Dict[str, Any]: An empty dictionary. + """ + return { + } + + RETURN_TYPES: tuple = ("PTVTRANSFORM",) + FUNCTION: str = "f" + CATEGORY: str = "Training" + + def f(self) -> tuple: + """ + Transforms elements of dataset to PyTorch tensors. + + Returns: + tuple: A tuple containing the transform callable. + """ + c = transforms.Compose([transforms.ToTensor()]) + return(c,) + + diff --git a/modules/pytorch_wrapper/utils.py b/modules/pytorch_wrapper/utils.py new file mode 100644 index 0000000..d86a661 --- /dev/null +++ b/modules/pytorch_wrapper/utils.py @@ -0,0 +1,139 @@ +import os +import ast +from typing import Optional, Tuple, Union +import torch + + +DTYPE_MAPPING = { + "float32": torch.float32, + "float16": torch.float16, + "bfloat16": torch.bfloat16, + "float64": torch.float64, + "uint8": torch.uint8, + "int8": torch.int8, + "int16": torch.int16, + "int32": torch.int32, + "int64": torch.int64, + "bool": torch.bool +} + +def str_to_number(s: str) -> Union[int, float]: + """ + Converts a string representation of a number to an integer or float. + + Args: + s (str): The string to convert. + + Returns: + Union[int, float]: The converted number, either as an `int` if no decimal point is present or as a `float` otherwise. + + Raises: + ValueError: If the input string is empty. + """ + if s: + if "." in s: + return float(s) + return int(s) + raise ValueError("Input string cannot be empty.") + + +def str_to_number_with_default(s: str, n: Union[int, float]) -> Union[int, float]: + """ + Converts a string representation of a number to an integer or float, with a fallback default value. + + Args: + s (str): The string to convert. + n (Union[int, float]): The default value to return if `s` is empty. + + Returns: + Union[int, float]: The converted number if `s` is non-empty, otherwise `n`. + """ + return str_to_number(s) if s else n + +def get_resource_full_path(resource_name: str, resource_path: str) -> str: + """ + Resolves the absolute path to a resource file within the "resources" directory and + ensures that the directory exists. + + Args: + resource_path (str): The path to the resource file. If the path is a relative path, + the path is considered relative to the `resource` directory of this extension. + + Returns: + str: The absolute path to the resource file. + """ + if os.path.isabs(resource_path): + resource_full_path = resource_path + else: + resource_full_path = os.path.realpath( + os.path.join(os.path.dirname(__file__), "..", "..", resource_name, resource_path)) + resource_dir = os.path.dirname(resource_full_path) + if os.path.exists(resource_dir) is False: + os.makedirs(resource_dir) + return resource_full_path + + +def get_model_full_path(model_path: str) -> str: + """ + Resolves the absolute path to a model file within the "models" directory and + ensures that the directory exists. + + Args: + model_path (str): The path to the model file. If the path is a relative path, + the path is considered relative to the `models` directory of this extension. + + Returns: + str: The absolute path to the model file. + """ + return get_resource_full_path("models", model_path) + + +def get_dataset_full_path(dataset_path: str) -> str: + """ + Resolves the absolute path to the datasete within the "datasets" directory and + ensures that the directory exists. + + Args: + dataset_path (str): The path to the dataset file. If the path is a relative path, + the path is considered relative to the `datasets` directory of this extension. + + Returns: + str: The absolute path to the dataset file. + """ + return get_resource_full_path("datasets", dataset_path) + + +def str_to_dim(dim: str) -> Optional[Union[int, Tuple[int, ...]]]: + """ + Converts a string representation of a dimension into an integer or a tuple of integers. + + Args: + dim (str): A string representing an integer or a tuple of integers. + + Returns: + Optional[Union[int, Tuple[int, ...]]]: + - An integer if the input represents a single integer. + - A tuple of integers if the input represents a list or tuple. + - None if the input is empty. + + Raises: + ValueError: If the parsed value is not an integer or a tuple of integers. + """ + if dim: + # Convert string to an integer, list, or tuple of integers + parsed_dim = ast.literal_eval(dim) + + # Convert list to tuple + if isinstance(parsed_dim, list): + parsed_dim = tuple(parsed_dim) + + # Ensure the parsed dim is valid + if not isinstance(parsed_dim, (int, tuple)) or ( + isinstance(parsed_dim, tuple) and not all(isinstance(i, int) for i in parsed_dim) + ): + raise ValueError(f"Invalid format for dim: {parsed_dim}. Must be an integer or a tuple of integers.") + + return parsed_dim + + return None + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b0cabd7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "pt-wrapper" +description = "PyTorch extension for ComfyUI featuring extensive PyTorch wrapper nodes for seamless tensor operations and PyTorch model training." +version = "3.0.0" +license = {file = "LICENSE"} + +[project.urls] +Repository = "https://github.com/HowToSD/ComfyUI-Data-Analysis" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "how-to-sd" +DisplayName = "ComfyUI-Pt-Wrapper" +Icon = "" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..42b57c1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +matplotlib>=3.10.0 +pandas>=2.2.3 +seaborn>=0.13.2 +scipy>=1.14.1 +scikit-learn>=1.6.1 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytorch_wrapper/__init__.py b/tests/pytorch_wrapper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pytorch_wrapper/pt_abs_test.py b/tests/pytorch_wrapper/pt_abs_test.py new file mode 100644 index 0000000..046e88a --- /dev/null +++ b/tests/pytorch_wrapper/pt_abs_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_abs import PtAbs # Updated import to PtAbs + + +class TestPtAbs(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtAbs() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([-1, 2, -3, 4], dtype=torch.float32), torch.tensor([1, 2, 3, 4], dtype=torch.float32)), # Mixed positive/negative + (torch.tensor([0, -5, 10, -15], dtype=torch.int32), torch.tensor([0, 5, 10, 15], dtype=torch.int32)), # Integer case + (torch.tensor([[-1.5, 2.3], [-3.7, 4.9]], dtype=torch.float32), torch.tensor([[1.5, 2.3], [3.7, 4.9]], dtype=torch.float32)), # 2D case + (torch.tensor([[-100, 0, 50], [-25, -75, 100]], dtype=torch.int64), torch.tensor([[100, 0, 50], [25, 75, 100]], dtype=torch.int64)), # Large numbers + (torch.full((2, 2), -7.7, dtype=torch.float32), torch.full((2, 2), 7.7, dtype=torch.float32)), # Constant matrix + ] + + def test_absolute_value(self): + """Test tensor element-wise absolute value using torch.abs().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected absolute value operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_acos_test.py b/tests/pytorch_wrapper/pt_acos_test.py new file mode 100644 index 0000000..528acf8 --- /dev/null +++ b/tests/pytorch_wrapper/pt_acos_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_acos import PtAcos # Updated import to PtAcos + + +class TestPtAcos(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtAcos() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float32), torch.acos(torch.tensor([1.0, 0.0, -1.0], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.acos(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.acos(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.acos(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_acos(self): + """Test tensor element-wise arc cosine using torch.acos().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected acos values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_add_test.py b/tests/pytorch_wrapper/pt_add_test.py new file mode 100644 index 0000000..a46d181 --- /dev/null +++ b/tests/pytorch_wrapper/pt_add_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_add import PtAdd + + +class TestPtAdd(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtAdd() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1.0, 2.0, 3.0]), torch.tensor([4.0, 5.0, 6.0]), torch.tensor([5.0, 7.0, 9.0])), + (torch.ones(3, 3), torch.ones(3, 3), torch.full((3, 3), 2.0)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.ones(2, 2)), + (torch.tensor([[1.0], [2.0]]), torch.tensor([[3.0], [4.0]]), torch.tensor([[4.0], [6.0]])), + ] + + def test_addition(self): + """Test tensor addition.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected sum + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_arange_test.py b/tests/pytorch_wrapper/pt_arange_test.py new file mode 100644 index 0000000..d80f56c --- /dev/null +++ b/tests/pytorch_wrapper/pt_arange_test.py @@ -0,0 +1,44 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_arange import PtArange + + +class TestPtArange(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtArange() + + # Test cases: (start, end, step, dtype, expected_tensor, expected_dtype) + self.test_cases = [ + ("0", "5", "1", "int32", torch.tensor([0, 1, 2, 3, 4], dtype=torch.int32), torch.int32), # Simple range + ("-3", "3", "1", "float32", torch.tensor([-3, -2, -1, 0, 1, 2], dtype=torch.float32), torch.float32), # Negative start + ("1.5", "4.5", "1", "float64", torch.tensor([1.5, 2.5, 3.5], dtype=torch.float64), torch.float64), # Floating point step + ("10", "0", "-2", "int64", torch.tensor([10, 8, 6, 4, 2], dtype=torch.int64), torch.int64), # Decreasing range + ("", "5", "1", "int8", torch.tensor([0, 1, 2, 3, 4], dtype=torch.int8), torch.int8), # Default start (0) + ("0", "10", "", "uint8", torch.tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=torch.uint8), torch.uint8), # Default step (1) + ] + + def test_arange_tensor_creation(self): + """Test tensor generation using torch.arange(), including values and dtype.""" + for start, end, step, dtype, expected, expected_dtype in self.test_cases: + with self.subTest(start=start, end=end, step=step, dtype=dtype): + result, = self.node.f(start, end, step, dtype) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected tensor values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + # Ensure result has the correct dtype + self.assertEqual(result.dtype, expected_dtype, f"Expected dtype {expected_dtype}, got {result.dtype}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_argmax_test.py b/tests/pytorch_wrapper/pt_argmax_test.py new file mode 100644 index 0000000..2be354b --- /dev/null +++ b/tests/pytorch_wrapper/pt_argmax_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_argmax import PtArgmax + + +class TestPtArgmax(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtArgmax() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + (torch.tensor([[1, 3, 2], [4, 0, 5]]), "0", False, torch.tensor([1, 0, 1])), + (torch.tensor([[1, 3, 2], [4, 0, 5]]), "1", True, torch.tensor([[1], [2]])), + (torch.tensor([[10, 3, 2], [123, 0, 5]]), "", False, torch.tensor(3)), + ] + + def test_argmax(self): + """Test torch.argmax behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected indices + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_argmin_test.py b/tests/pytorch_wrapper/pt_argmin_test.py new file mode 100644 index 0000000..89753c9 --- /dev/null +++ b/tests/pytorch_wrapper/pt_argmin_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_argmin import PtArgmin + + +class TestPtArgmin(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtArgmin() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + (torch.tensor([[1, 3, 2], [4, 0, 5]]), "0", False, torch.tensor([0, 1, 0])), + (torch.tensor([[1, 3, 2], [4, 0, 5]]), "1", True, torch.tensor([[0], [1]])), + (torch.tensor([[1, 3, 2], [4, 0, 5]]), "", False, torch.tensor(4)), + ] + + def test_argmin(self): + """Test torch.argmin behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected indices + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_asin_test.py b/tests/pytorch_wrapper/pt_asin_test.py new file mode 100644 index 0000000..92f8905 --- /dev/null +++ b/tests/pytorch_wrapper/pt_asin_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_asin import PtAsin # Updated import to PtAsin + + +class TestPtAsin(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtAsin() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float32), torch.asin(torch.tensor([1.0, 0.0, -1.0], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.asin(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.asin(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.asin(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_asin(self): + """Test tensor element-wise arc sine using torch.asin().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected asin values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_atan_test.py b/tests/pytorch_wrapper/pt_atan_test.py new file mode 100644 index 0000000..551d7a5 --- /dev/null +++ b/tests/pytorch_wrapper/pt_atan_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_atan import PtAtan # Updated import to PtAtan + + +class TestPtAtan(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtAtan() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1.0, 0.0, -1.0], dtype=torch.float32), torch.atan(torch.tensor([1.0, 0.0, -1.0], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.atan(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.atan(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.atan(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_atan(self): + """Test tensor element-wise arc tangent using torch.atan().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected atan values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bitwise_and_test.py b/tests/pytorch_wrapper/pt_bitwise_and_test.py new file mode 100644 index 0000000..630d459 --- /dev/null +++ b/tests/pytorch_wrapper/pt_bitwise_and_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bitwise_and import PtBitwiseAnd # Updated import to PtBitwiseAnd + + +class TestPtBitwiseAnd(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBitwiseAnd() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([0b0001, 0b0011, 0b0101]), torch.tensor([0b0001, 0b0010, 0b0110]), torch.tensor([0b0001, 0b0010, 0b0100])), + (torch.tensor([0b1111, 0b1010, 0b1100]), torch.tensor([0b0101, 0b1100, 0b1010]), torch.tensor([0b0101, 0b1000, 0b1000])), + (torch.tensor([1, 2, 3]), torch.tensor([3, 2, 1]), torch.tensor([1, 2, 1])), + (torch.ones(2, 2, dtype=torch.int32), torch.zeros(2, 2, dtype=torch.int32), torch.zeros(2, 2, dtype=torch.int32)), + (torch.full((2, 2), 0b1010, dtype=torch.int32), torch.full((2, 2), 0b1100, dtype=torch.int32), torch.full((2, 2), 0b1000, dtype=torch.int32)), + ] + + def test_bitwise_and(self): + """Test tensor element-wise bitwise AND using torch.bitwise_and().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected bitwise AND operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bitwise_left_shift_test.py b/tests/pytorch_wrapper/pt_bitwise_left_shift_test.py new file mode 100644 index 0000000..124b8bf --- /dev/null +++ b/tests/pytorch_wrapper/pt_bitwise_left_shift_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bitwise_left_shift import PtBitwiseLeftShift # Updated import to PtBitwiseLeftShift + + +class TestPtBitwiseLeftShift(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBitwiseLeftShift() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 4], dtype=torch.int64), torch.tensor([1, 2, 3], dtype=torch.int64), torch.tensor([2, 8, 32], dtype=torch.int64)), # 1<<1, 2<<2, 4<<3 + (torch.tensor([4, 8, 16], dtype=torch.int64), torch.tensor([2, 1, 3], dtype=torch.int64), torch.tensor([16, 16, 128], dtype=torch.int64)), # 4<<2, 8<<1, 16<<3 + (torch.tensor([0b0001, 0b0010, 0b0100], dtype=torch.int64), torch.tensor([1, 2, 3], dtype=torch.int64), torch.tensor([0b0010, 0b1000, 0b100000], dtype=torch.int64)), # Bitwise shift + (torch.ones(3, 3, dtype=torch.int64), torch.full((3, 3), 2, dtype=torch.int64), torch.full((3, 3), 4, dtype=torch.int64)), # 1<<2 = 4 + (torch.full((2, 2), 5, dtype=torch.int64), torch.full((2, 2), 3, dtype=torch.int64), torch.full((2, 2), 40, dtype=torch.int64)), # 5<<3 = 40 + ] + + def test_bitwise_left_shift(self): + """Test tensor element-wise bitwise left shift using torch.bitwise_left_shift().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected bitwise left shift operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bitwise_not_test.py b/tests/pytorch_wrapper/pt_bitwise_not_test.py new file mode 100644 index 0000000..a7d87d8 --- /dev/null +++ b/tests/pytorch_wrapper/pt_bitwise_not_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bitwise_not import PtBitwiseNot # Updated import to PtBitwiseNot + + +class TestPtBitwiseNot(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBitwiseNot() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0, 1, 2, 3], dtype=torch.int32), torch.tensor([-1, -2, -3, -4], dtype=torch.int32)), # Bitwise NOT + (torch.tensor([0b0000, 0b0001, 0b0010], dtype=torch.int32), torch.tensor([-1, -2, -3], dtype=torch.int32)), # Binary representation + (torch.tensor([[5, 10], [15, 20]], dtype=torch.int32), torch.tensor([[-6, -11], [-16, -21]], dtype=torch.int32)), # Matrix case + (torch.full((2, 2), 0, dtype=torch.int32), torch.full((2, 2), -1, dtype=torch.int32)), # NOT 0 -> -1 + (torch.full((3, 3), -1, dtype=torch.int32), torch.full((3, 3), 0, dtype=torch.int32)), # NOT -1 -> 0 + ] + + def test_bitwise_not(self): + """Test tensor element-wise bitwise NOT using torch.bitwise_not().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected bitwise NOT operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bitwise_or_test.py b/tests/pytorch_wrapper/pt_bitwise_or_test.py new file mode 100644 index 0000000..9b606fd --- /dev/null +++ b/tests/pytorch_wrapper/pt_bitwise_or_test.py @@ -0,0 +1,54 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bitwise_or import PtBitwiseOr # Updated import to PtBitwiseOr + + +class TestPtBitwiseOr(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBitwiseOr() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([0b0001, 0b0011, 0b0101], dtype=torch.int32), + torch.tensor([0b0010, 0b0100, 0b0110], dtype=torch.int32), + torch.tensor([0b0011, 0b0111, 0b0111], dtype=torch.int32)), # Binary OR + + (torch.tensor([1, 2, 4], dtype=torch.int32), + torch.tensor([8, 16, 32], dtype=torch.int32), + torch.tensor([9, 18, 36], dtype=torch.int32)), # Decimal OR + + (torch.tensor([5, 10, 15], dtype=torch.int32), + torch.tensor([3, 6, 12], dtype=torch.int32), + torch.tensor([7, 14, 15], dtype=torch.int32)), # Mixed values + + (torch.zeros(3, 3, dtype=torch.int32), + torch.ones(3, 3, dtype=torch.int32), + torch.ones(3, 3, dtype=torch.int32)), # OR with all ones + + (torch.full((2, 2), 0b1010, dtype=torch.int32), + torch.full((2, 2), 0b1100, dtype=torch.int32), + torch.full((2, 2), 0b1110, dtype=torch.int32)), # OR with constant matrices + ] + + def test_bitwise_or(self): + """Test tensor element-wise bitwise OR using torch.bitwise_or().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected bitwise OR operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bitwise_right_shift_test.py b/tests/pytorch_wrapper/pt_bitwise_right_shift_test.py new file mode 100644 index 0000000..69a9b9b --- /dev/null +++ b/tests/pytorch_wrapper/pt_bitwise_right_shift_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bitwise_right_shift import PtBitwiseRightShift # Updated import to PtBitwiseRightShift + + +class TestPtBitwiseRightShift(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBitwiseRightShift() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([4, 8, 16], dtype=torch.int64), torch.tensor([1, 2, 3], dtype=torch.int64), torch.tensor([2, 2, 2], dtype=torch.int64)), # 4>>1, 8>>2, 16>>3 + (torch.tensor([32, 64, 128], dtype=torch.int64), torch.tensor([2, 3, 4], dtype=torch.int64), torch.tensor([8, 8, 8], dtype=torch.int64)), # 32>>2, 64>>3, 128>>4 + (torch.tensor([0b1000, 0b10000, 0b100000], dtype=torch.int64), torch.tensor([1, 2, 3], dtype=torch.int64), torch.tensor([0b0100, 0b00100, 0b000100], dtype=torch.int64)), # Bitwise shift + (torch.full((3, 3), 16, dtype=torch.int64), torch.full((3, 3), 2, dtype=torch.int64), torch.full((3, 3), 4, dtype=torch.int64)), # 16>>2 = 4 + (torch.full((2, 2), 40, dtype=torch.int64), torch.full((2, 2), 3, dtype=torch.int64), torch.full((2, 2), 5, dtype=torch.int64)), # 40>>3 = 5 + ] + + def test_bitwise_right_shift(self): + """Test tensor element-wise bitwise right shift using torch.bitwise_right_shift().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected bitwise right shift operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bitwise_xor_test.py b/tests/pytorch_wrapper/pt_bitwise_xor_test.py new file mode 100644 index 0000000..24ce242 --- /dev/null +++ b/tests/pytorch_wrapper/pt_bitwise_xor_test.py @@ -0,0 +1,54 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bitwise_xor import PtBitwiseXor # Updated import to PtBitwiseXor + + +class TestPtBitwiseXor(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBitwiseXor() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([0b0001, 0b0011, 0b0101], dtype=torch.int32), + torch.tensor([0b0010, 0b0100, 0b0110], dtype=torch.int32), + torch.tensor([0b0011, 0b0111, 0b0011], dtype=torch.int32)), # Binary XOR + + (torch.tensor([1, 2, 4], dtype=torch.int32), + torch.tensor([8, 16, 32], dtype=torch.int32), + torch.tensor([9, 18, 36], dtype=torch.int32)), # Decimal XOR + + (torch.tensor([5, 10, 15], dtype=torch.int32), + torch.tensor([3, 6, 12], dtype=torch.int32), + torch.tensor([6, 12, 3], dtype=torch.int32)), # Mixed values + + (torch.zeros(3, 3, dtype=torch.int32), + torch.ones(3, 3, dtype=torch.int32), + torch.ones(3, 3, dtype=torch.int32)), # XOR with all ones + + (torch.full((2, 2), 0b1010, dtype=torch.int32), + torch.full((2, 2), 0b1100, dtype=torch.int32), + torch.full((2, 2), 0b0110, dtype=torch.int32)), # XOR with constant matrices + ] + + def test_bitwise_xor(self): + """Test tensor element-wise bitwise XOR using torch.bitwise_xor().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected bitwise XOR operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bmm_test.py b/tests/pytorch_wrapper/pt_bmm_test.py new file mode 100644 index 0000000..e1e1a87 --- /dev/null +++ b/tests/pytorch_wrapper/pt_bmm_test.py @@ -0,0 +1,60 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bmm import PtBmm # Updated import to PtBmm + + +class TestPtBmm(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtBmm() + + # Valid test cases: (Tensor A, Tensor B, Expected result) + self.valid_test_cases = [ + # Case 1: Batch of 2 matrices (3x4) * (4x5) → (2, 3, 5) + (torch.ones(2, 3, 4), torch.ones(2, 4, 5), torch.full((2, 3, 5), 4.0)), + + # Case 2: Batch of 3 matrices (2x3) * (3x2) → (3, 2, 2) + (torch.tensor([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]]), + torch.tensor([[[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]]]), + torch.tensor([[[58.0, 64.0], [139.0, 154.0]]])), + + # Case 3: Identity matrix batch multiplication (should remain the same) + (torch.eye(3).expand(4, -1, -1), torch.eye(3).expand(4, -1, -1), torch.eye(3).expand(4, -1, -1)), + ] + + # Invalid test cases (should raise errors) + self.invalid_test_cases = [ + (torch.ones(3, 4), torch.ones(4, 5)), # 2D tensors (not allowed) + (torch.ones(2, 3, 4), torch.ones(3, 4, 5)), # Mismatched batch size + (torch.ones(2, 3, 4, 5), torch.ones(2, 5, 6)), # 4D tensors (not allowed) + (torch.ones(2, 3, 4), torch.ones(2, 5, 6)), # Matmul shape mismatch within batch + ] + + def test_batched_matrix_multiplication(self): + """Test valid batched matrix multiplication cases.""" + for tens_a, tens_b, expected in self.valid_test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected batch matrix multiplication + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_invalid_multiplication(self): + """Test invalid cases where dimensions do not match.""" + for tens_a, tens_b in self.invalid_test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + with self.assertRaises(Exception, msg=f"torch.bmm() should fail for shapes {tens_a.shape} and {tens_b.shape}"): + self.node.f(tens_a, tens_b) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_bool_create_test.py b/tests/pytorch_wrapper/pt_bool_create_test.py new file mode 100644 index 0000000..8e3ce6e --- /dev/null +++ b/tests/pytorch_wrapper/pt_bool_create_test.py @@ -0,0 +1,58 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_bool_create import PtBoolCreate + +class TestPtBoolCreate(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtBoolCreate() + + # Test cases covering various ranks + self.test_cases = [ + # Rank 0 (scalar) + ("True", torch.tensor(True, dtype=torch.bool)), + ("False", torch.tensor(False, dtype=torch.bool)), + + # Rank 1 (1D array) + ("[True, False, True]", torch.tensor([True, False, True], dtype=torch.bool)), + + # Rank 2 (2D matrix) + ("[[True, False], [False, True]]", torch.tensor([[True, False], [False, True]], dtype=torch.bool)), + + # Rank 3 (3D tensor) + ( + "[[[True, False], [False, True]], [[False, True], [True, False]]]", + torch.tensor([[[True, False], [False, True]], [[False, True], [True, False]]], dtype=torch.bool), + ), + ] + + def test_create_tensor(self): + """Test PyTorch boolean tensor creation from various structured inputs.""" + for data_str, expected_tensor in self.test_cases: + with self.subTest(data=data_str): + retval = self.node.f(data_str)[0] + + # Ensure the returned value is a PyTorch tensor + self.assertTrue(isinstance(retval, torch.Tensor), f"Expected tensor, got {type(retval)}") + + # Ensure the dtype is bool + self.assertEqual(retval.dtype, torch.bool, f"Dtype mismatch for input: {data_str}") + + # Ensure the shape matches + self.assertEqual(retval.shape, expected_tensor.shape, + f"Shape mismatch for input: {data_str} (Expected {expected_tensor.shape}, got {retval.shape})") + + # Ensure values match + torch.testing.assert_close(retval, expected_tensor, + msg=f"Value mismatch for input: {data_str}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_cos_test.py b/tests/pytorch_wrapper/pt_cos_test.py new file mode 100644 index 0000000..9d03217 --- /dev/null +++ b/tests/pytorch_wrapper/pt_cos_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_cos import PtCos # Updated import to PtCos + + +class TestPtCos(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtCos() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, torch.pi / 2, torch.pi], dtype=torch.float32), torch.cos(torch.tensor([0.0, torch.pi / 2, torch.pi], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.cos(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.cos(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.cos(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_cos(self): + """Test tensor element-wise cosine using torch.cos().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected cos values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_cosh_test.py b/tests/pytorch_wrapper/pt_cosh_test.py new file mode 100644 index 0000000..299fec0 --- /dev/null +++ b/tests/pytorch_wrapper/pt_cosh_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_cosh import PtCosh # Updated import to PtCosh + + +class TestPtCosh(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtCosh() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, 1.0, -1.0], dtype=torch.float32), torch.cosh(torch.tensor([0.0, 1.0, -1.0], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.cosh(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.cosh(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.cosh(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_cosh(self): + """Test tensor element-wise hyperbolic cosine using torch.cosh().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected cosh values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_crop_test.py b/tests/pytorch_wrapper/pt_crop_test.py new file mode 100644 index 0000000..8ebe074 --- /dev/null +++ b/tests/pytorch_wrapper/pt_crop_test.py @@ -0,0 +1,49 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.pt_crop import PtCrop + + +class TestPtCrop(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtCrop() + + # Test cases: (Input tensor, Height, Width, Expected shape) + self.test_cases = [ + (torch.rand(1, 3, 16, 16), 8, 8, (1, 3, 8, 8)), # Crop 4D tensor + (torch.rand(3, 16, 16), 8, 8, (3, 8, 8)), # Crop 3D tensor + (torch.rand(1, 3, 16, 16), 16, 16, (1, 3, 16, 16)), # No crop, same size + ] + + def test_crop(self): + """Test tensor cropping to specific sizes.""" + for tens, height, width, expected_shape in self.test_cases: + with self.subTest(height=height, width=width): + result, = self.node.f(tens, height, width) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result shape matches expected shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + + def test_invalid_tensor_rank(self): + """Test that an invalid tensor rank raises an error.""" + tens = torch.rand(16, 16) # 2D tensor (invalid) + with self.assertRaises(ValueError, msg="Only rank 3 or 4 tensors are supported for crop."): + self.node.f(tens, 8, 8) + + def test_crop_exceeds_dimensions(self): + """Test that cropping beyond tensor dimensions raises an error.""" + tens = torch.rand(1, 3, 16, 16) + with self.assertRaises(ValueError, msg="Specified crop size exceeds tensor dimensions."): + self.node.f(tens, 32, 32) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_data_loader_test.py b/tests/pytorch_wrapper/pt_data_loader_test.py new file mode 100644 index 0000000..6b59a75 --- /dev/null +++ b/tests/pytorch_wrapper/pt_data_loader_test.py @@ -0,0 +1,43 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.ptv_dataset import PtvDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor +from pytorch_wrapper.pt_data_loader import PtDataLoader + +class TestPtDataLoader(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.dataset_node = PtvDataset() + self.compose_node = PtvTransformsToTensor() + self.data_loader_node = PtDataLoader() + + def test_with_transform(self): + """Tests with transform""" + bs = 2 + transform = self.compose_node.f()[0] + dataset = self.dataset_node.f("MNIST", + True, # download + "", # root + '{"train": True}', + transform=transform)[0] + train_data = self.data_loader_node.f( + dataset=dataset, + batch_size=bs, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + x, y = next(iter(train_data)) + self.assertEqual(x.size(), torch.Size([bs, 1, 28, 28])) + self.assertEqual(y.size(), torch.Size([bs])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_div_test.py b/tests/pytorch_wrapper/pt_div_test.py new file mode 100644 index 0000000..7e6e022 --- /dev/null +++ b/tests/pytorch_wrapper/pt_div_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_div import PtDiv # Updated import to PtDiv + + +class TestPtDiv(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtDiv() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([4.0, 10.0, 18.0]), torch.tensor([2.0, 5.0, 6.0]), torch.tensor([2.0, 2.0, 3.0])), + (torch.ones(3, 3), torch.ones(3, 3), torch.ones(3, 3)), # 1 / 1 = 1 + (torch.full((2, 2), 4.0), torch.full((2, 2), 2.0), torch.full((2, 2), 2.0)), # 4 / 2 = 2 + (torch.tensor([[6.0], [12.0]]), torch.tensor([[3.0], [4.0]]), torch.tensor([[2.0], [3.0]])), # Element-wise division + ] + + def test_division(self): + """Test tensor division.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected quotient + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_einsum_test.py b/tests/pytorch_wrapper/pt_einsum_test.py new file mode 100644 index 0000000..ef93fcb --- /dev/null +++ b/tests/pytorch_wrapper/pt_einsum_test.py @@ -0,0 +1,99 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_einsum import PtEinsum # Updated import to PtEinsum + + +class TestPtEinsum(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtEinsum() + + def test_trace_extraction(self): + """Extract the diagonal (1,2,3,4,5) and sum it up (trace).""" + matrix = torch.tensor([[1, 0, 0, 0, 0], + [0, 2, 0, 0, 0], + [0, 0, 3, 0, 0], + [0, 0, 0, 4, 0], + [0, 0, 0, 0, 5]], dtype=torch.float32) + expected = torch.tensor(15.0) # Sum of diagonal + result, = self.node.f("ii", matrix) # Only requires `tens_a` + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_diagonal_extraction(self): + """Extract diagonal elements (1,2,3,4,5).""" + matrix = torch.tensor([[1, 0, 0, 0, 0], + [0, 2, 0, 0, 0], + [0, 0, 3, 0, 0], + [0, 0, 0, 4, 0], + [0, 0, 0, 0, 5]], dtype=torch.float32) + expected = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float32) + result, = self.node.f("ii->i", matrix) # Only requires `tens_a` + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_hadamard_product(self): + """Compute Hadamard product (element-wise multiplication).""" + a = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=torch.float32) + b = torch.tensor([[9, 10, 11, 12], [13, 14, 15, 16]], dtype=torch.float32) + expected = a * b + result, = self.node.f("ij,ij->ij", a, b) + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_matrix_multiplication(self): + """Test standard matrix multiplication.""" + a = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=torch.float32) + b = torch.tensor([[9, 10], [11, 12], [13, 14], [15, 16]], dtype=torch.float32) + expected = torch.mm(a, b) + result, = self.node.f("ij,jk->ik", a, b) + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_3d_tensor_multiplication(self): + """Test matrix multiplication for rank-3 tensors.""" + a = torch.ones(2, 3, 4, dtype=torch.float32) + b = torch.ones(2, 4, 5, dtype=torch.float32) + expected = torch.matmul(a, b) + result, = self.node.f("bij,bjk->bik", a, b) + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_4d_tensor_multiplication(self): + """Test matrix multiplication for rank-4 tensors.""" + a = torch.ones(2, 3, 4, 6, dtype=torch.float32) # (batch, groups, rows, cols) + b = torch.ones(2, 3, 6, 5, dtype=torch.float32) # (batch, groups, cols, out_features) + + expected = torch.matmul(a, b) # Standard 4D matrix multiplication + result, = self.node.f("bijk,bikl->bijl", a, b) # Correct einsum notation + + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_bilinear_transformation(self): + """Test bilinear transformation: a vector, W matrix, and b vector.""" + a = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float32) + W = torch.tensor([[6, 7, 8, 9, 10], + [11, 12, 13, 14, 15], + [16, 17, 18, 19, 20], + [21, 22, 23, 24, 25], + [26, 27, 28, 29, 30]], dtype=torch.float32) + b = torch.tensor([31, 32, 33, 34, 35], dtype=torch.float32) + + expected = torch.dot(a, torch.matmul(W, b)) + result, = self.node.f("i,ij,j->", a, W, b) + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_tensor_format_conversion(self): + """Test conversion from (B, C, H, W) format to (B, H, W, C) format.""" + a = torch.randn(2, 3, 64, 64, dtype=torch.float32) # (Batch, Channels, Height, Width) + + expected = a.permute(0, 2, 3, 1) # Convert to (Batch, Height, Width, Channels) + result, = self.node.f("bchw->bhwc", a) # Correct einsum notation for format swap + + self.assertTrue(torch.equal(result, expected), f"Expected {expected.shape}, got {result.shape}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_eq_test.py b/tests/pytorch_wrapper/pt_eq_test.py new file mode 100644 index 0000000..0943a8f --- /dev/null +++ b/tests/pytorch_wrapper/pt_eq_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_eq import PtEq # Updated import to PtEq + + +class TestPtEq(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtEq() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 3]), torch.tensor([1, 2, 3]), torch.tensor([True, True, True])), + (torch.tensor([1, 2, 3]), torch.tensor([3, 2, 1]), torch.tensor([False, True, False])), + (torch.ones(3, 3), torch.ones(3, 3), torch.full((3, 3), True, dtype=torch.bool)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.full((2, 2), False, dtype=torch.bool)), + (torch.tensor([[1], [2]]), torch.tensor([[1], [3]]), torch.tensor([[True], [False]])), + ] + + def test_equality(self): + """Test tensor element-wise equality using torch.eq().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected element-wise equality + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_evaluate_classification_conv_model_test.py b/tests/pytorch_wrapper/pt_evaluate_classification_conv_model_test.py new file mode 100644 index 0000000..78e2509 --- /dev/null +++ b/tests/pytorch_wrapper/pt_evaluate_classification_conv_model_test.py @@ -0,0 +1,83 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_conv_model import PtnConvModel +from pytorch_wrapper.ptv_dataset import PtvDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_evaluate_classification_model import PtEvaluateClassificationModel +from pytorch_wrapper.pt_load_model import PtLoadModel + +class TestPtEvaluate(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + # Transform + compose_node = PtvTransformsToTensor() + transform = compose_node.f()[0] + + # Dataset + dataset_node = PtvDataset() + dataset = dataset_node.f("FashionMNIST", + True, # download + "", # root + '{"train": False}', + transform=transform)[0] + + # Data loader + data_loader_node = PtDataLoader() + self.data_loader = data_loader_node.f( + dataset=dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + # Model + self.model_node = PtnConvModel() + self.model = self.model_node.f( + input_dim="(1, 28, 28)", + penultimate_dim=0, + output_dim=10, + channel_list="[32,64,128,256]", + kernel_size_list="[3,3,3,3]", + padding_list="[1,1,1,1]", + downsample_list="[True,True,True,True]" + )[0] + + # Load weights + self.load_model = PtLoadModel() + epochs = 1 + self.loaded_model = self.load_model.f(self.model, f"fashion_mnist_{epochs}_epochs_conv.pt") + + # Instantiates evaluation node + self.evaluate = PtEvaluateClassificationModel() + + def test_evaluation(self): + """ + Tests evaluation. + + Note that this test requires a trained model file on the file system. + Unit test will fail if you run this for the first time. + Run the unit test for training to generate the weight, and re-run this unit test. + """ + accuracy, precision, recall, f1, num_samples = \ + self.evaluate.f(self.model, self.data_loader, use_gpu=True) + print(f""" + accuracy: {accuracy} + precision: {precision} + recall: {recall} + f1: {f1} + Number of samples: {num_samples} + """) + self.assertTrue(accuracy > 0.8) + self.assertEqual(num_samples, 10000) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_evaluate_classification_conv_model_test2.py b/tests/pytorch_wrapper/pt_evaluate_classification_conv_model_test2.py new file mode 100644 index 0000000..cf6419e --- /dev/null +++ b/tests/pytorch_wrapper/pt_evaluate_classification_conv_model_test2.py @@ -0,0 +1,83 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_conv_model import PtnConvModel +from pytorch_wrapper.ptv_image_folder_dataset import PtvImageFolderDataset +from pytorch_wrapper.ptv_transforms_resize import PtvTransformsResize +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_evaluate_classification_model import PtEvaluateClassificationModel +from pytorch_wrapper.pt_load_model import PtLoadModel + +class TestPtEvaluate(unittest.TestCase): + + @unittest.skipIf(os.getenv("RUN_SKIPPED_TESTS") != "1", "Skipping unless explicitly enabled") + def setUp(self): + """Set up test instance.""" + # Transform + compose_node = PtvTransformsResize() + transform = compose_node.f(256, 256)[0] + + # Dataset + self.node = PtvImageFolderDataset() + dataset = self.node.f("dog_and_cat/val", + transform=transform)[0] + + # Data loader + data_loader_node = PtDataLoader() + self.data_loader = data_loader_node.f( + dataset=dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + # Model + self.model_node = PtnConvModel() + self.model = self.model_node.f( + input_dim="(3, 256, 256)", + penultimate_dim=0, + output_dim=2, + channel_list="[32,64,128,256,512]", # To 128, 64, 32, 16, 8 + kernel_size_list="[3,3,3,3,3]", + padding_list="[1,1,1,1,1]", + downsample_list="[True,True,True,True,True]" + )[0] + + # Load weights + self.load_model = PtLoadModel() + epochs = 20 + self.loaded_model = self.load_model.f(self.model,f"dog_cat_{epochs}_epochs_conv.pt") + + # Instantiates evaluation node + self.evaluate = PtEvaluateClassificationModel() + + @unittest.skipIf(os.getenv("RUN_SKIPPED_TESTS") != "1", "Skipping unless explicitly enabled") + def test_evaluation(self): + """ + Tests evaluation. + + Note that this test requires a trained model file on the file system. + Unit test will fail if you run this for the first time. + Run the unit test for training to generate the weight, and re-run this unit test. + """ + accuracy, precision, recall, f1, num_samples = \ + self.evaluate.f(self.model, self.data_loader, use_gpu=True) + print(f""" + accuracy: {accuracy} + precision: {precision} + recall: {recall} + f1: {f1} + Number of samples: {num_samples} + """) + self.assertTrue(accuracy > 0.7) + self.assertEqual(num_samples, 300) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_evaluate_classification_model_test.py b/tests/pytorch_wrapper/pt_evaluate_classification_model_test.py new file mode 100644 index 0000000..262e13a --- /dev/null +++ b/tests/pytorch_wrapper/pt_evaluate_classification_model_test.py @@ -0,0 +1,79 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_linear_model import PtnLinearModel +from pytorch_wrapper.ptv_dataset import PtvDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_evaluate_classification_model import PtEvaluateClassificationModel +from pytorch_wrapper.pt_load_model import PtLoadModel + +class TestPtEvaluate(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + # Transform + compose_node = PtvTransformsToTensor() + transform = compose_node.f()[0] + + # Dataset + dataset_node = PtvDataset() + dataset = dataset_node.f("FashionMNIST", + True, # download + "", # root + '{"train": False}', + transform=transform)[0] + + # Data loader + data_loader_node = PtDataLoader() + self.data_loader = data_loader_node.f( + dataset=dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + # Model + self.model_node = PtnLinearModel() + self.model = self.model_node.f( + "784,180,42,10", # input dim for first layer, output dim for each layer + "True,True,True", # bias + 3 # num layers + )[0] + + # Load weights + self.load_model = PtLoadModel() + epochs = 1 + self.loaded_model = self.load_model.f(self.model, f"fashion_mnist_{epochs}_epochs.pt") + + # Instntiates evaluation node + self.evaluate = PtEvaluateClassificationModel() + + def test_evaluation(self): + """ + Tests evaluation. + + Note that this test requires a trained model file on the file system. + Unit test will fail if you run this for the first time. + Run the unit test for training to generate the weight, and re-run this unit test. + """ + accuracy, precision, recall, f1, num_samples = \ + self.evaluate.f(self.model, self.data_loader, use_gpu=True) + print(f""" + accuracy: {accuracy} + precision: {precision} + recall: {recall} + f1: {f1} + Number of samples: {num_samples} + """) + self.assertTrue(accuracy > 0.8) + self.assertEqual(num_samples, 10000) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_exp_test.py b/tests/pytorch_wrapper/pt_exp_test.py new file mode 100644 index 0000000..b88551c --- /dev/null +++ b/tests/pytorch_wrapper/pt_exp_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_exp import PtExp # Updated import to PtExp + + +class TestPtExp(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtExp() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, 1.0, 2.0], dtype=torch.float32), torch.exp(torch.tensor([0.0, 1.0, 2.0], dtype=torch.float32))), # Edge cases + (torch.tensor([-1.0, -2.0, 0.5], dtype=torch.float32), torch.exp(torch.tensor([-1.0, -2.0, 0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.exp(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.exp(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_exp(self): + """Test tensor element-wise exponential function using torch.exp().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected exp values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_flatten_test.py b/tests/pytorch_wrapper/pt_flatten_test.py new file mode 100644 index 0000000..5a66695 --- /dev/null +++ b/tests/pytorch_wrapper/pt_flatten_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_flatten import PtFlatten + + +class TestPtFlatten(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtFlatten() + + # Test cases: (Input tensor, Expected flattened tensor shape) + self.test_cases = [ + (torch.randn(3, 4, 5), (60,)), # 3D Tensor + (torch.randn(2, 3, 4, 5), (120,)), # 4D Tensor + (torch.randn(10, 10), (100,)), # 2D Tensor + (torch.randn(1), (1,)), # 1D Tensor remains unchanged + ] + + def test_flatten(self): + """Test flattening tensors correctly.""" + for tensor, expected_shape in self.test_cases: + with self.subTest(tensor_shape=tensor.shape): + flattened, = self.node.f(tensor) + + # Ensure return value is a tensor + self.assertTrue(isinstance(flattened, torch.Tensor), "Output is not a tensor") + + # Ensure shape matches expected flattened shape + self.assertEqual(flattened.shape, expected_shape, f"Shape mismatch: expected {expected_shape}, got {flattened.shape}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_float_create_test.py b/tests/pytorch_wrapper/pt_float_create_test.py new file mode 100644 index 0000000..f5acb08 --- /dev/null +++ b/tests/pytorch_wrapper/pt_float_create_test.py @@ -0,0 +1,65 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_float_create import PtFloatCreate + +class TestPtFloatCreate(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtFloatCreate() + + # Test cases covering rank 0 (scalar) to rank 5 tensors + self.test_cases = [ + # Rank 0 (scalar) + ("12345", torch.tensor(12345, dtype=torch.float32)), + ("12345.6", torch.tensor(12345.6, dtype=torch.float32)), + + # Rank 1 (1D array) + ("[1, 2, 3]", torch.tensor([1, 2, 3], dtype=torch.float32)), + ("[1.1, 2.2, 3.3]", torch.tensor([1.1, 2.2, 3.3], dtype=torch.float32)), + + # Rank 2 (2D matrix) + ("[[1, 2, 3], [4, 5, 6]]", torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)), + ("[[1.1, 2.2], [3.3, 4.4]]", torch.tensor([[1.1, 2.2], [3.3, 4.4]], dtype=torch.float32)), + + # Rank 3 (3D tensor) + ("[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]", + torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=torch.float32)), + + # Rank 4 (4D tensor) + ("[[[[1, 2], [3, 4]]], [[[5, 6], [7, 8]]]]", + torch.tensor([[[[1, 2], [3, 4]]], [[[5, 6], [7, 8]]]], dtype=torch.float32)), + + # Rank 5 (5D tensor) + ("[[[[[1], [2]], [[3], [4]]]], [[[[5], [6]], [[7], [8]]]]]", + torch.tensor([[[[[1], [2]], [[3], [4]]]], [[[[5], [6]], [[7], [8]]]]], dtype=torch.float32)), + ] + + def test_create_tensor(self): + """Test PyTorch tensor creation from various structured inputs.""" + for data_str, expected_tensor in self.test_cases: + with self.subTest(data=data_str): + retval = self.node.f(data_str)[0] + + # Ensure the returned value is a PyTorch tensor + self.assertTrue(isinstance(retval, torch.Tensor), f"Expected tensor, got {type(retval)}") + + # Ensure the dtype is float32 + self.assertEqual(retval.dtype, torch.float32, f"Dtype mismatch for input: {data_str}") + + # Ensure the shape matches + self.assertEqual(retval.shape, expected_tensor.shape, + f"Shape mismatch for input: {data_str} (Expected {expected_tensor.shape}, got {retval.shape})") + + # Ensure values match + torch.testing.assert_close(retval, expected_tensor, + msg=f"Value mismatch for input: {data_str}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_floor_divide_test.py b/tests/pytorch_wrapper/pt_floor_divide_test.py new file mode 100644 index 0000000..9347727 --- /dev/null +++ b/tests/pytorch_wrapper/pt_floor_divide_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_floor_divide import PtFloorDiv + + +class TestPtFloorDiv(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtFloorDiv() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([4.0, 10.0, 18.0]), torch.tensor([3.0, 4.0, 5.0]), torch.tensor([1.0, 2.0, 3.0])), + (torch.ones(3, 3) * 7, torch.ones(3, 3) * 3, torch.ones(3, 3) * 2), # 7 // 3 = 2 + (torch.full((2, 2), 9.0), torch.full((2, 2), 2.0), torch.full((2, 2), 4.0)), # 9 // 2 = 4 + (torch.tensor([[7.0], [15.0]]), torch.tensor([[2.0], [4.0]]), torch.tensor([[3.0], [3.0]])), # Element-wise floor division + ] + + def test_floor_division(self): + """Test tensor floor division.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected floor quotient + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_from_image_transpose_test.py b/tests/pytorch_wrapper/pt_from_image_transpose_test.py new file mode 100644 index 0000000..6a2b74a --- /dev/null +++ b/tests/pytorch_wrapper/pt_from_image_transpose_test.py @@ -0,0 +1,57 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_from_image_transpose import PtFromImageTranspose + + +class TestPtFromImageTranspose(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtFromImageTranspose() + + # Define test cases: (input_tensor, expected_shape) + self.test_cases = [ + # Case 1: 3D tensor (H, W, C) -> (C, H, W) + (torch.ones(64, 128, 3), (3, 64, 128)), # (64,128,3) -> (3,64,128) + + # Case 2: 4D tensor (N, H, W, C) -> (N, C, H, W) + (torch.ones(10, 64, 128, 3), (10, 3, 64, 128)), # (10,64,128,3) -> (10,3,64,128) + + # Case 3: Invalid input (2D tensor) + (torch.ones(64, 128), ValueError), # 2D tensors should raise an error + + # Case 4: Invalid input (5D tensor) + (torch.ones(10, 64, 128, 3, 2), ValueError), # 5D tensors should raise an error + ] + + def test_transpose(self): + """Test image transposition operation with valid and invalid cases.""" + for input_tensor, expected in self.test_cases: + with self.subTest(input_shape=input_tensor.shape): + if isinstance(expected, tuple): + # Valid cases + result, = self.node.f(input_tensor) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected), f"Expected shape {expected}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after transpose operation") + else: + # Invalid cases should raise an error + with self.assertRaises(expected): + self.node.f(input_tensor) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_from_numpy_test.py b/tests/pytorch_wrapper/pt_from_numpy_test.py new file mode 100644 index 0000000..49e6642 --- /dev/null +++ b/tests/pytorch_wrapper/pt_from_numpy_test.py @@ -0,0 +1,65 @@ +import os +import sys +import unittest +import torch +import numpy as np + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_from_numpy import PtFromNumpy + + +class TestPtFromNumpy(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtFromNumpy() + + # Test cases covering rank 0 (scalar) to rank 3 tensors + self.test_cases = [ + # Rank 0 (scalar) int32 + (np.array(12345, dtype=np.int32), torch.tensor(12345, dtype=torch.int32)), + # Rank 0 (scalar) float32 + (np.array(12345.6, dtype=np.float32), torch.tensor(12345.6, dtype=torch.float32)), + + # Rank 1 (1D array) int32 + (np.array([1, 2, 3], dtype=np.int32), torch.tensor([1, 2, 3], dtype=torch.int32)), + # Rank 1 (1D array) float32 + (np.array([1.1, 2.2, 3.3], dtype=np.float32), torch.tensor([1.1, 2.2, 3.3], dtype=torch.float32)), + + # Rank 2 (2D matrix) int32 + (np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32), torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32)), + # Rank 2 (2D matrix) float32 + (np.array([[1.1, 2.2], [3.3, 4.4]], dtype=np.float32), torch.tensor([[1.1, 2.2], [3.3, 4.4]], dtype=torch.float32)), + + # Rank 3 (3D tensor) int32 + (np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=np.int32), + torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=torch.int32)), + # Rank 3 (3D tensor) float32 + (np.array([[[1.1, 2.2], [3.3, 4.4]], [[5.5, 6.6], [7.7, 8.8]]], dtype=np.float32), + torch.tensor([[[1.1, 2.2], [3.3, 4.4]], [[5.5, 6.6], [7.7, 8.8]]], dtype=torch.float32)), + ] + + def test_convert_numpy_to_tensor(self): + """Test conversion of NumPy ndarray to PyTorch tensor.""" + for np_array, expected_tensor in self.test_cases: + with self.subTest(data=np_array): + retval = self.node.f(np_array)[0] + + # Ensure the returned value is a PyTorch tensor + self.assertTrue(isinstance(retval, torch.Tensor), f"Expected tensor, got {type(retval)}") + + # Ensure the dtype matches + self.assertEqual(retval.dtype, expected_tensor.dtype, f"Dtype mismatch for input: {np_array}") + + # Ensure the shape matches + self.assertEqual(retval.shape, expected_tensor.shape, + f"Shape mismatch for input: {np_array} (Expected {expected_tensor.shape}, got {retval.shape})") + + # Ensure values match + torch.testing.assert_close(retval, expected_tensor, + msg=f"Value mismatch for input: {np_array}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_full_test.py b/tests/pytorch_wrapper/pt_full_test.py new file mode 100644 index 0000000..77f35de --- /dev/null +++ b/tests/pytorch_wrapper/pt_full_test.py @@ -0,0 +1,71 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_full import PtFull +from pytorch_wrapper.utils import DTYPE_MAPPING + + +class TestPtFull(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtFull() + + # Tensor size to test + self.size_str = "[2, 3]" # Using a small 2D tensor size for all tests + self.expected_shape = (2, 3) + + # Values to test (using representative values for each type) + self.test_values = { + "float32": "1.5", + "float16": "-2.3", + "bfloat16": "3.7", + "float64": "4.9", + "uint8": "255", # Max value for uint8 + "int8": "-128", # Min value for int8 + "int16": "32767", # Max value for int16 + "int32": "-2147483648", # Min value for int32 + "int64": "9223372036854775807", # Max value for int64 + "bool": "True" + } + + def test_full_tensor_all_dtypes(self): + """Test tensor creation with a specified value for all supported data types.""" + for data_type, torch_dtype in DTYPE_MAPPING.items(): + value_str = self.test_values[data_type] + + with self.subTest(data_type=data_type, value=value_str): + result, = self.node.f(value_str, self.size_str, data_type) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(self.expected_shape), f"Expected shape {self.expected_shape}, got {result.shape}") + + # Ensure result has correct dtype + self.assertEqual(result.dtype, torch_dtype, f"Expected dtype {torch_dtype}, got {result.dtype}") + + # Convert value_str to correct type + if torch_dtype in {torch.float32, torch.float16, torch.bfloat16, torch.float64}: + expected_value = float(value_str) + elif torch_dtype in {torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64}: + expected_value = int(value_str) + elif torch_dtype == torch.bool: + expected_value = value_str.lower() == "true" + else: + raise ValueError(f"Unsupported data type: {data_type}") + + # Ensure all elements match the expected value + expected = torch.full(self.expected_shape, expected_value, dtype=torch_dtype) + self.assertTrue(torch.equal(result, expected), f"Tensor values do not match expected tensor for dtype {data_type}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_gather_test.py b/tests/pytorch_wrapper/pt_gather_test.py new file mode 100644 index 0000000..df09c79 --- /dev/null +++ b/tests/pytorch_wrapper/pt_gather_test.py @@ -0,0 +1,48 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_gather import PtGather + + +class TestPtGather(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtGather() + + # Test cases: (Tensor, Dim, Index (as string), Expected result) + self.test_cases = [ + ( + torch.tensor([[10, 20, 30, 40, 50], [100, 200, 300, 400, 500]]), + 1, + "[[0, 4, 3, 2, 0], [4, 3, 0, 0, 0]]", + torch.tensor([[10, 50, 40, 30, 10], [500, 400, 100, 100, 100]]) + ), + ( + torch.tensor([[1, 2], [3, 4]]), + 0, + "[[1, 0], [0, 1]]", + torch.tensor([[3, 2], [1, 4]]) + ), + ] + + def test_gather(self): + """Test torch.gather behavior.""" + for tens, dim, index, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, index=index): + result, = self.node.f(tens, dim, index) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected gathered values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_ge_test.py b/tests/pytorch_wrapper/pt_ge_test.py new file mode 100644 index 0000000..92ab581 --- /dev/null +++ b/tests/pytorch_wrapper/pt_ge_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_ge import PtGe # Updated import to PtGe + + +class TestPtGe(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtGe() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 3]), torch.tensor([0, 2, 4]), torch.tensor([True, True, False])), + (torch.tensor([3, 5, 7]), torch.tensor([1, 5, 10]), torch.tensor([True, True, False])), + (torch.ones(3, 3), torch.ones(3, 3), torch.full((3, 3), True, dtype=torch.bool)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.full((2, 2), False, dtype=torch.bool)), + (torch.tensor([[5], [10]]), torch.tensor([[5], [10]]), torch.tensor([[True], [True]])), + ] + + def test_greater_or_equal(self): + """Test tensor element-wise greater-than-or-equal condition using torch.ge().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected greater-than-or-equal comparison + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_gt_test.py b/tests/pytorch_wrapper/pt_gt_test.py new file mode 100644 index 0000000..29603bf --- /dev/null +++ b/tests/pytorch_wrapper/pt_gt_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_gt import PtGt # Updated import to PtGt + + +class TestPtGt(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtGt() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 3]), torch.tensor([0, 2, 4]), torch.tensor([True, False, False])), + (torch.tensor([3, 5, 7]), torch.tensor([1, 5, 10]), torch.tensor([True, False, False])), + (torch.ones(3, 3), torch.zeros(3, 3), torch.full((3, 3), True, dtype=torch.bool)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.full((2, 2), False, dtype=torch.bool)), + (torch.tensor([[5], [10]]), torch.tensor([[3], [10]]), torch.tensor([[True], [False]])), + ] + + def test_greater_than(self): + """Test tensor element-wise greater-than condition using torch.gt().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected greater-than comparison + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_index_select_test.py b/tests/pytorch_wrapper/pt_index_select_test.py new file mode 100644 index 0000000..8751214 --- /dev/null +++ b/tests/pytorch_wrapper/pt_index_select_test.py @@ -0,0 +1,48 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_index_select import PtIndexSelect + + +class TestPtIndexSelect(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtIndexSelect() + + # Test cases: (Tensor, Dim, Index (as string), Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 2, 3], [40, 50, 60], [700, 800, 900]]), + 0, + "[0, 2]", + torch.tensor([[1, 2, 3], [700, 800, 900]]) + ), + ( + torch.tensor([[1, 2, 3], [40, 50, 60], [700, 800, 900]]), + 1, + "[1, 2]", + torch.tensor([[2, 3], [50, 60], [800, 900]]) + ), + ] + + def test_index_select(self): + """Test torch.index_select behavior.""" + for tens, dim, index, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, index=index): + result, = self.node.f(tens, dim, index) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected selected values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_int_create_test.py b/tests/pytorch_wrapper/pt_int_create_test.py new file mode 100644 index 0000000..d77df2e --- /dev/null +++ b/tests/pytorch_wrapper/pt_int_create_test.py @@ -0,0 +1,62 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_int_create import PtIntCreate + +class TestPtIntCreate(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtIntCreate() + + # Test cases covering rank 0 (scalar) to rank 5 tensors + self.test_cases = [ + # Rank 0 (scalar) + ("12345", torch.tensor(12345, dtype=torch.int32)), + + # Rank 1 (1D array) + ("[1, 2, 3]", torch.tensor([1, 2, 3], dtype=torch.int32)), + + # Rank 2 (2D matrix) + ("[[1, 2, 3], [4, 5, 6]]", torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32)), + + # Rank 3 (3D tensor) + ("[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]", + torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=torch.int32)), + + # Rank 4 (4D tensor) + ("[[[[1, 2], [3, 4]]], [[[5, 6], [7, 8]]]]", + torch.tensor([[[[1, 2], [3, 4]]], [[[5, 6], [7, 8]]]], dtype=torch.int32)), + + # Rank 5 (5D tensor) + ("[[[[[1], [2]], [[3], [4]]]], [[[[5], [6]], [[7], [8]]]]]", + torch.tensor([[[[[1], [2]], [[3], [4]]]], [[[[5], [6]], [[7], [8]]]]], dtype=torch.int32)), + ] + + def test_create_tensor(self): + """Test PyTorch tensor creation from various structured inputs.""" + for data_str, expected_tensor in self.test_cases: + with self.subTest(data=data_str): + retval = self.node.f(data_str)[0] + + # Ensure the returned value is a PyTorch tensor + self.assertTrue(isinstance(retval, torch.Tensor), f"Expected tensor, got {type(retval)}") + + # Ensure the dtype is int32 + self.assertEqual(retval.dtype, torch.int32, f"Dtype mismatch for input: {data_str}") + + # Ensure the shape matches + self.assertEqual(retval.shape, expected_tensor.shape, + f"Shape mismatch for input: {data_str} (Expected {expected_tensor.shape}, got {retval.shape})") + + # Ensure values match + torch.testing.assert_close(retval, expected_tensor, + msg=f"Value mismatch for input: {data_str}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_interpolate_by_scale_factor_test.py b/tests/pytorch_wrapper/pt_interpolate_by_scale_factor_test.py new file mode 100644 index 0000000..07422d0 --- /dev/null +++ b/tests/pytorch_wrapper/pt_interpolate_by_scale_factor_test.py @@ -0,0 +1,60 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_interpolate_by_scale_factor import PtInterpolateByScaleFactor + + +class TestPtInterpolateByScaleFactor(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtInterpolateByScaleFactor() + + # Test cases 3d: (Input tensor, Scale factor, Mode, Expected shape) + self.test_cases_3d = [ + (torch.rand(3, 16, 16), 2.0, "nearest", (3, 32, 32)), + (torch.rand(3, 16, 16), 2.0, "nearest-exact", (3, 32, 32)), + (torch.rand(3, 16, 16), 0.5, "bilinear", (3, 8, 8)), + (torch.rand(3, 16, 16), 1.5, "bicubic", (3, 24, 24)), + ] + + # Test cases: (Input tensor, Scale factor, Mode, Expected shape) + self.test_cases_4d = [ + (torch.rand(1, 3, 16, 16), 2.0, "nearest", (1, 3, 32, 32)), # Upscale 4D tensor + (torch.rand(1, 3, 16, 16), 2.0, "nearest-exact", (1, 3, 32, 32)), # Upscale 4D tensor + (torch.rand(1, 3, 16, 16), 0.5, "bilinear", (1, 3, 8, 8)), # Downscale 4D tensor + (torch.rand(1, 3, 16, 16), 1.5, "bicubic", (1, 3, 24, 24)), # Bicubic mode with 4D tensor + ] + + def test_interpolation_3d(self): + """Test tensor interpolation with different scale factors and modes.""" + for tens, scale_factor, mode, expected_shape in self.test_cases_3d: + with self.subTest(scale_factor=scale_factor, mode=mode): + result, = self.node.f(tens, scale_factor, mode) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result shape matches expected shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + + + def test_interpolation_4d(self): + """Test tensor interpolation with different scale factors and modes.""" + for tens, scale_factor, mode, expected_shape in self.test_cases_4d: + with self.subTest(scale_factor=scale_factor, mode=mode): + result, = self.node.f(tens, scale_factor, mode) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result shape matches expected shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_interpolate_to_size_test.py b/tests/pytorch_wrapper/pt_interpolate_to_size_test.py new file mode 100644 index 0000000..36baf26 --- /dev/null +++ b/tests/pytorch_wrapper/pt_interpolate_to_size_test.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_interpolate_to_size import PtInterpolateToSize + + +class TestPtInterpolateToSize(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtInterpolateToSize() + + # Test cases 3d: (Input tensor, Height, Width, Mode, Expected shape) + self.test_cases_3d = [ + (torch.rand(3, 16, 16), 32, 32, "nearest", (3, 32, 32)), # Upscale 3D tensor + (torch.rand(3, 16, 16), 32, 32, "nearest-exact", (3, 32, 32)), # Upscale 3D tensor + (torch.rand(3, 16, 16), 8, 8, "bilinear", (3, 8, 8)), # Downscale 3D tensor + (torch.rand(3, 16, 16), 24, 24, "bicubic", (3, 24, 24)), # Bicubic mode with 3D tensor + ] + + # Test cases 4d: (Input tensor, Height, Width, Mode, Expected shape) + self.test_cases_4d = [ + (torch.rand(1, 3, 16, 16), 32, 32, "nearest", (1, 3, 32, 32)), # Upscale 4D tensor + (torch.rand(1, 3, 16, 16), 32, 32, "nearest-exact", (1, 3, 32, 32)), # Upscale 4D tensor + (torch.rand(1, 3, 16, 16), 8, 8, "bilinear", (1, 3, 8, 8)), # Downscale 4D tensor + (torch.rand(1, 3, 16, 16), 24, 24, "bicubic", (1, 3, 24, 24)), # Bicubic mode with 4D tensor + ] + + def test_interpolation_3d(self): + """Test tensor interpolation with different target sizes and modes.""" + for tens, height, width, mode, expected_shape in self.test_cases_3d: + with self.subTest(height=height, width=width, mode=mode): + result, = self.node.f(tens, height, width, mode) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result shape matches expected shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + + + def test_interpolation_4d(self): + """Test tensor interpolation with different target sizes and modes.""" + for tens, height, width, mode, expected_shape in self.test_cases_4d: + with self.subTest(height=height, width=width, mode=mode): + result, = self.node.f(tens, height, width, mode) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result shape matches expected shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_le_test.py b/tests/pytorch_wrapper/pt_le_test.py new file mode 100644 index 0000000..4cb062d --- /dev/null +++ b/tests/pytorch_wrapper/pt_le_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_le import PtLe # Updated import to PtLe + + +class TestPtLe(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLe() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 3]), torch.tensor([0, 2, 4]), torch.tensor([False, True, True])), + (torch.tensor([3, 5, 7]), torch.tensor([1, 5, 10]), torch.tensor([False, True, True])), + (torch.ones(3, 3), torch.ones(3, 3), torch.full((3, 3), True, dtype=torch.bool)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.full((2, 2), True, dtype=torch.bool)), + (torch.tensor([[5], [10]]), torch.tensor([[5], [10]]), torch.tensor([[True], [True]])), + ] + + def test_less_or_equal(self): + """Test tensor element-wise less-than-or-equal condition using torch.le().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected less-than-or-equal comparison + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_linspace_test.py b/tests/pytorch_wrapper/pt_linspace_test.py new file mode 100644 index 0000000..3678a1f --- /dev/null +++ b/tests/pytorch_wrapper/pt_linspace_test.py @@ -0,0 +1,43 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_linspace import PtLinspace + + +class TestPtLinspace(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLinspace() + + # Test cases: (start, end, steps, dtype, expected_tensor, expected_dtype) + self.test_cases = [ + ("0", "10", "5", "float32", torch.tensor([0, 2.5, 5, 7.5, 10], dtype=torch.float32), torch.float32), # Basic float32 case + ("-5", "5", "6", "float64", torch.tensor([-5, -3, -1, 1, 3, 5], dtype=torch.float64), torch.float64), # Negative to positive + ("1", "2", "3", "float16", torch.tensor([1, 1.5, 2], dtype=torch.float16), torch.float16), # Small float16 case + ("100", "200", "5", "int32", torch.tensor([100, 125, 150, 175, 200], dtype=torch.int32), torch.int32), # Large int32 case + ("0", "1", "2", "bfloat16", torch.tensor([0, 1], dtype=torch.bfloat16), torch.bfloat16), # Simple two-step bfloat16 + ] + + def test_linspace_tensor_creation(self): + """Test tensor generation using torch.linspace(), including values and dtype.""" + for start, end, steps, dtype, expected, expected_dtype in self.test_cases: + with self.subTest(start=start, end=end, steps=steps, dtype=dtype): + result, = self.node.f(start, end, steps, dtype) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected tensor values + self.assertTrue(torch.allclose(result, expected, atol=1e-5), f"Expected {expected}, got {result}") + + # Ensure result has the correct dtype + self.assertEqual(result.dtype, expected_dtype, f"Expected dtype {expected_dtype}, got {result.dtype}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_log_test.py b/tests/pytorch_wrapper/pt_log_test.py new file mode 100644 index 0000000..a2b644a --- /dev/null +++ b/tests/pytorch_wrapper/pt_log_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_log import PtLog # Updated import to PtLog + + +class TestPtLog(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLog() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1.0, torch.e, 10.0], dtype=torch.float32), torch.log(torch.tensor([1.0, torch.e, 10.0], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, 2.0, 3.0], dtype=torch.float32), torch.log(torch.tensor([0.5, 2.0, 3.0], dtype=torch.float32))), # Common values + (torch.tensor([[1.5, 3.2], [4.7, 5.9]], dtype=torch.float32), torch.log(torch.tensor([[1.5, 3.2], [4.7, 5.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 2.5, dtype=torch.float32), torch.log(torch.full((2, 2), 2.5, dtype=torch.float32))), # Constant matrix + ] + + def test_log(self): + """Test tensor element-wise natural logarithm using torch.log().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected log values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_logical_and_test.py b/tests/pytorch_wrapper/pt_logical_and_test.py new file mode 100644 index 0000000..c5227fb --- /dev/null +++ b/tests/pytorch_wrapper/pt_logical_and_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_logical_and import PtLogicalAnd # Updated import to PtLogicalAnd + + +class TestPtLogicalAnd(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLogicalAnd() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([True, False, True]), torch.tensor([False, False, True]), torch.tensor([False, False, True])), + (torch.tensor([[True, True], [False, False]]), torch.tensor([[True, False], [True, False]]), torch.tensor([[True, False], [False, False]])), + (torch.ones(3, 3, dtype=torch.bool), torch.zeros(3, 3, dtype=torch.bool), torch.zeros(3, 3, dtype=torch.bool)), # All true AND all false -> all false + (torch.ones(2, 2, dtype=torch.bool), torch.ones(2, 2, dtype=torch.bool), torch.ones(2, 2, dtype=torch.bool)), # All true AND all true -> all true + (torch.tensor([1, 0, 1, 0], dtype=torch.bool), torch.tensor([1, 1, 0, 0], dtype=torch.bool), torch.tensor([True, False, False, False])), # Mixed cases + ] + + def test_logical_and(self): + """Test tensor element-wise logical AND using torch.logical_and().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected logical AND operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_logical_not_test.py b/tests/pytorch_wrapper/pt_logical_not_test.py new file mode 100644 index 0000000..de18c94 --- /dev/null +++ b/tests/pytorch_wrapper/pt_logical_not_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_logical_not import PtLogicalNot # Updated import to PtLogicalNot + + +class TestPtLogicalNot(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLogicalNot() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([True, False, True]), torch.tensor([False, True, False])), + (torch.tensor([[True, True], [False, False]]), torch.tensor([[False, False], [True, True]])), + (torch.ones(3, 3, dtype=torch.bool), torch.zeros(3, 3, dtype=torch.bool)), # NOT all true -> all false + (torch.zeros(2, 2, dtype=torch.bool), torch.ones(2, 2, dtype=torch.bool)), # NOT all false -> all true + (torch.tensor([1, 0, 1, 0], dtype=torch.bool), torch.tensor([False, True, False, True])), # Mixed cases + ] + + def test_logical_not(self): + """Test tensor element-wise logical NOT using torch.logical_not().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected logical NOT operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_logical_or_test.py b/tests/pytorch_wrapper/pt_logical_or_test.py new file mode 100644 index 0000000..97ae602 --- /dev/null +++ b/tests/pytorch_wrapper/pt_logical_or_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_logical_or import PtLogicalOr # Updated import to PtLogicalOr + + +class TestPtLogicalOr(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLogicalOr() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([True, False, True]), torch.tensor([False, False, True]), torch.tensor([True, False, True])), + (torch.tensor([[True, True], [False, False]]), torch.tensor([[False, True], [True, False]]), torch.tensor([[True, True], [True, False]])), + (torch.ones(3, 3, dtype=torch.bool), torch.zeros(3, 3, dtype=torch.bool), torch.ones(3, 3, dtype=torch.bool)), # All true OR all false -> all true + (torch.zeros(2, 2, dtype=torch.bool), torch.zeros(2, 2, dtype=torch.bool), torch.zeros(2, 2, dtype=torch.bool)), # All false OR all false -> all false + (torch.tensor([1, 0, 1, 0], dtype=torch.bool), torch.tensor([1, 1, 0, 0], dtype=torch.bool), torch.tensor([True, True, True, False])), # Mixed cases + ] + + def test_logical_or(self): + """Test tensor element-wise logical OR using torch.logical_or().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected logical OR operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_logical_xor_test.py b/tests/pytorch_wrapper/pt_logical_xor_test.py new file mode 100644 index 0000000..7756330 --- /dev/null +++ b/tests/pytorch_wrapper/pt_logical_xor_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_logical_xor import PtLogicalXor # Updated import to PtLogicalXor + + +class TestPtLogicalXor(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLogicalXor() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([True, False, True]), torch.tensor([False, False, True]), torch.tensor([True, False, False])), + (torch.tensor([[True, True], [False, False]]), torch.tensor([[False, True], [True, False]]), torch.tensor([[True, False], [True, False]])), + (torch.ones(3, 3, dtype=torch.bool), torch.zeros(3, 3, dtype=torch.bool), torch.ones(3, 3, dtype=torch.bool)), # True XOR False -> True + (torch.zeros(2, 2, dtype=torch.bool), torch.zeros(2, 2, dtype=torch.bool), torch.zeros(2, 2, dtype=torch.bool)), # False XOR False -> False + (torch.tensor([1, 0, 1, 0], dtype=torch.bool), torch.tensor([1, 1, 0, 0], dtype=torch.bool), torch.tensor([False, True, True, False])), # Mixed cases + ] + + def test_logical_xor(self): + """Test tensor element-wise logical XOR using torch.logical_xor().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected logical XOR operation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_lt_test.py b/tests/pytorch_wrapper/pt_lt_test.py new file mode 100644 index 0000000..027c478 --- /dev/null +++ b/tests/pytorch_wrapper/pt_lt_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_lt import PtLt # Updated import to PtLt + + +class TestPtLt(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtLt() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 3]), torch.tensor([0, 2, 4]), torch.tensor([False, False, True])), + (torch.tensor([3, 5, 7]), torch.tensor([1, 5, 10]), torch.tensor([False, False, True])), + (torch.ones(3, 3), torch.ones(3, 3), torch.full((3, 3), False, dtype=torch.bool)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.full((2, 2), True, dtype=torch.bool)), + (torch.tensor([[5], [10]]), torch.tensor([[5], [10]]), torch.tensor([[False], [False]])), + ] + + def test_less_than(self): + """Test tensor element-wise less-than condition using torch.lt().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected less-than comparison + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_masked_select_test.py b/tests/pytorch_wrapper/pt_masked_select_test.py new file mode 100644 index 0000000..7ff2303 --- /dev/null +++ b/tests/pytorch_wrapper/pt_masked_select_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_masked_select import PtMaskedSelect + + +class TestPtMaskedSelect(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMaskedSelect() + + # Test cases: (Tensor, Masked Tensor, Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 2, 3], [40, 50, 60], [700, 800, 900]]), + torch.tensor([[True, False, False], [False, True, False], [False, False, True]]), + torch.tensor([1, 50, 900]) + ), + ] + + def test_masked_select(self): + """Test torch.masked_select behavior.""" + for tens, masked_tens, expected in self.test_cases: + with self.subTest(tens=tens, masked_tens=masked_tens): + result, = self.node.f(tens, masked_tens) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected selected values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_matmul_test.py b/tests/pytorch_wrapper/pt_matmul_test.py new file mode 100644 index 0000000..d3f5245 --- /dev/null +++ b/tests/pytorch_wrapper/pt_matmul_test.py @@ -0,0 +1,63 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_matmul import PtMatMul # Updated import to PtMatMul + + +class TestPtMatMul(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMatMul() + + # Valid test cases: (Tensor A, Tensor B, Expected result) + self.valid_test_cases = [ + # Case 1: 1D x 1D (dot product) + (torch.tensor([1.0, 2.0, 3.0]), torch.tensor([4.0, 5.0, 6.0]), torch.tensor(32.0)), + + # Case 2: 1D x 2D (vector-matrix multiplication) + (torch.tensor([1.0, 2.0]), torch.tensor([[3.0, 4.0], [5.0, 6.0]]), torch.tensor([13.0, 16.0])), + + # Case 3: 2D x 2D (matrix-matrix multiplication) + (torch.tensor([[1.0, 2.0], [3.0, 4.0]]), torch.tensor([[5.0, 6.0], [7.0, 8.0]]), torch.tensor([[19.0, 22.0], [43.0, 50.0]])), + + # Case 4: 3D x 2D (batched matrix-matrix multiplication) + (torch.ones(2, 3, 4), torch.ones(4, 5), torch.full((2, 3, 5), 4.0)), # (2,3,4) * (4,5) → (2,3,5) + + # Case 5: 3D x 3D (batched matrix multiplication with broadcasting) + (torch.ones(2, 3, 4), torch.ones(2, 4, 5), torch.full((2, 3, 5), 4.0)), # (2,3,4) * (2,4,5) → (2,3,5) + ] + + # Invalid test cases (should raise errors) + self.invalid_test_cases = [ + (torch.ones(2, 3), torch.ones(4, 2)), # (2,3) * (4,2) is invalid + (torch.tensor([1.0, 2.0, 3.0]), torch.tensor([4.0, 5.0])), # Vector size mismatch + (torch.ones(2, 3, 4), torch.ones(3, 5)), # (2,3,4) * (3,5) should fail due to misalignment + ] + + def test_matrix_multiplication(self): + """Test valid matrix multiplication cases.""" + for tens_a, tens_b, expected in self.valid_test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected matrix multiplication + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_invalid_multiplication(self): + """Test invalid cases where dimensions do not match.""" + for tens_a, tens_b in self.invalid_test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + with self.assertRaises(RuntimeError, msg=f"MatMul should fail for shapes {tens_a.shape} and {tens_b.shape}"): + self.node.f(tens_a, tens_b) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_max_test.py b/tests/pytorch_wrapper/pt_max_test.py new file mode 100644 index 0000000..d5d2a8a --- /dev/null +++ b/tests/pytorch_wrapper/pt_max_test.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_max import PtMax + + +class TestPtMax(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMax() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "0", + False, + torch.tensor([4, 3, 5]) + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "1", + True, + torch.tensor([[3], [5]]) + ), + ( + torch.tensor([[[1, 5], [2, 8]], [[3, 4], [6, 7]]]), + "(0, 1)", + False, + torch.tensor([6, 8]) + ), + ( + # Reduce across all axes + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "", # Passing empty string to reduce across all dimensions + False, + torch.tensor(5) + ) + ] + + def test_max(self): + """Test torch.max behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected max values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_mean_test.py b/tests/pytorch_wrapper/pt_mean_test.py new file mode 100644 index 0000000..4f5f2c1 --- /dev/null +++ b/tests/pytorch_wrapper/pt_mean_test.py @@ -0,0 +1,87 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_mean import PtMean + + +class TestPtMean(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMean() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + # Float tensor cases + ( + torch.tensor([[1.0, 3.0, 2.0], [4.0, 0.0, 5.0]], dtype=torch.float), + "0", + False, + torch.tensor([2.5, 1.5, 3.5], dtype=torch.float) + ), + ( + torch.tensor([[1.0, 3.0, 2.0], [4.0, 0.0, 5.0]], dtype=torch.float), + "1", + True, + torch.tensor([[2.], [3.]], dtype=torch.float) + ), + ( + torch.tensor([[[1.0, 5.0], [2.0, 8.0]], [[3.0, 4.0], [6.0, 7.0]]], dtype=torch.float), + "(0, 1)", + False, + torch.tensor([3.0, 6.0], dtype=torch.float) + ), + ( + # Reduce across all axes + torch.tensor([[1.0, 3.0, 2.0], [4.0, 0.0, 5.0]], dtype=torch.float), + "", # Passing empty string to reduce across all dimensions + False, + torch.tensor(2.5, dtype=torch.float) + ), + # Complex tensor cases + ( + torch.tensor([[1 + 1j, 3 + 3j, 2 + 2j], [4 + 4j, 0 + 0j, 5 + 5j]], dtype=torch.complex64), + "0", + False, + torch.tensor([2.5 + 2.5j, 1.5 + 1.5j, 3.5 + 3.5j], dtype=torch.complex64) + ), + ( + torch.tensor([[1 + 1j, 3 + 3j, 2 + 2j], [4 + 4j, 0 + 0j, 5 + 5j]], dtype=torch.complex64), + "1", + True, + torch.tensor([[2. + 2j], [3. + 3j]], dtype=torch.complex64) + ), + ( + torch.tensor([[[1 + 1j, 5 + 5j], [2 + 2j, 8 + 8j]], [[3 + 3j, 4 + 4j], [6 + 6j, 7 + 7j]]], dtype=torch.complex64), + "(0, 1)", + False, + torch.tensor([3. + 3j, 6. + 6j], dtype=torch.complex64) + ), + ( + # Reduce across all axes + torch.tensor([[1 + 1j, 3 + 3j, 2 + 2j], [4 + 4j, 0 + 0j, 5 + 5j]], dtype=torch.complex64), + "", # Passing empty string to reduce across all dimensions + False, + torch.tensor(2.5 + 2.5j, dtype=torch.complex64) + ) + ] + + def test_mean(self): + """Test torch.mean behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected mean values + self.assertTrue(torch.allclose(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_median_test.py b/tests/pytorch_wrapper/pt_median_test.py new file mode 100644 index 0000000..15283c5 --- /dev/null +++ b/tests/pytorch_wrapper/pt_median_test.py @@ -0,0 +1,66 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_median import PtMedian + + +class TestPtMedian(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMedian() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 3, 2], [2, 1, 3], [4, 0, 5]]), + "0", + False, + torch.tensor([2, 1, 3]) + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "0", + False, + torch.tensor([1, 0, 2]) # Fix: torch.median returns the lower value when between two numbers + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "1", + True, + torch.tensor([[2], [4]]) + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5], [7, 8, 6], [9, 2, 10]]), + "0", + False, + torch.tensor([4, 2, 5]) + ), + ( + torch.tensor([[1, 3, 2, 8], [4, 0, 5, 6], [7, 8, 6, 9]]), + "1", + False, + torch.tensor([2, 4, 7]) + ) + ] + + def test_median(self): + """Test torch.median behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected median values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_min_test.py b/tests/pytorch_wrapper/pt_min_test.py new file mode 100644 index 0000000..f70194f --- /dev/null +++ b/tests/pytorch_wrapper/pt_min_test.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_min import PtMin + + +class TestPtMin(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMin() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "0", + False, + torch.tensor([1, 0, 2]) + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "1", + True, + torch.tensor([[1], [0]]) + ), + ( + torch.tensor([[[1, 5], [2, 8]], [[3, 4], [6, 7]]]), + "(0, 1)", + False, + torch.tensor([1, 4]) + ), + ( + # Reduce across all axes + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "", # Passing empty string to reduce across all dimensions + False, + torch.tensor(0) + ) + ] + + def test_amin(self): + """Test torch.amin behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected min values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_mm_test.py b/tests/pytorch_wrapper/pt_mm_test.py new file mode 100644 index 0000000..33fb78f --- /dev/null +++ b/tests/pytorch_wrapper/pt_mm_test.py @@ -0,0 +1,58 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_mm import PtMm # Updated import to PtMm + + +class TestPtMm(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMm() + + # Valid test cases: (Tensor A, Tensor B, Expected result) + self.valid_test_cases = [ + # Case 1: 2D x 2D (matrix-matrix multiplication) + (torch.tensor([[1.0, 2.0], [3.0, 4.0]]), torch.tensor([[5.0, 6.0], [7.0, 8.0]]), torch.tensor([[19.0, 22.0], [43.0, 50.0]])), + + # Case 2: 2D identity matrix multiplication (should remain the same) + (torch.eye(3), torch.eye(3), torch.eye(3)), + + # Case 3: Rectangular 2D matrix multiplication + (torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), torch.tensor([[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]]), torch.tensor([[58.0, 64.0], [139.0, 154.0]])), + ] + + # Invalid test cases (should raise errors) + self.invalid_test_cases = [ + (torch.tensor([1.0, 2.0, 3.0]), torch.tensor([4.0, 5.0, 6.0])), # 1D x 1D (dot product not supported by mm) + (torch.tensor([[1.0, 2.0]]), torch.tensor([3.0, 4.0])), # 2D x 1D mismatch + (torch.ones(2, 3, 4), torch.ones(4, 5)), # 3D x 2D is invalid for mm + (torch.ones(2, 3, 4), torch.ones(2, 4, 5)), # 3D x 3D is invalid for mm + ] + + def test_matrix_multiplication(self): + """Test valid matrix multiplication cases.""" + for tens_a, tens_b, expected in self.valid_test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected matrix multiplication + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + + def test_invalid_multiplication(self): + """Test invalid cases where dimensions do not match.""" + for tens_a, tens_b in self.invalid_test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + with self.assertRaises(ValueError, msg=f"torch.mm() should fail for shapes {tens_a.shape} and {tens_b.shape}"): + self.node.f(tens_a, tens_b) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_mul_test.py b/tests/pytorch_wrapper/pt_mul_test.py new file mode 100644 index 0000000..8bff3f9 --- /dev/null +++ b/tests/pytorch_wrapper/pt_mul_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_mul import PtMul # Updated import to PtMul + + +class TestPtMul(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtMul() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1.0, 2.0, 3.0]), torch.tensor([4.0, 5.0, 6.0]), torch.tensor([4.0, 10.0, 18.0])), + (torch.ones(3, 3), torch.ones(3, 3), torch.ones(3, 3)), # 1 * 1 = 1 + (torch.zeros(2, 2), torch.ones(2, 2), torch.zeros(2, 2)), # 0 * 1 = 0 + (torch.tensor([[2.0], [3.0]]), torch.tensor([[3.0], [4.0]]), torch.tensor([[6.0], [12.0]])), # Element-wise multiplication + ] + + def test_multiplication(self): + """Test tensor multiplication.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected product + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_ne_test.py b/tests/pytorch_wrapper/pt_ne_test.py new file mode 100644 index 0000000..4816b21 --- /dev/null +++ b/tests/pytorch_wrapper/pt_ne_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_ne import PtNe # Updated import to PtNe + + +class TestPtNe(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtNe() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([1, 2, 3]), torch.tensor([1, 2, 3]), torch.tensor([False, False, False])), + (torch.tensor([1, 2, 3]), torch.tensor([3, 2, 1]), torch.tensor([True, False, True])), + (torch.ones(3, 3), torch.ones(3, 3), torch.full((3, 3), False, dtype=torch.bool)), + (torch.zeros(2, 2), torch.ones(2, 2), torch.full((2, 2), True, dtype=torch.bool)), + (torch.tensor([[1], [2]]), torch.tensor([[1], [3]]), torch.tensor([[False], [True]])), + ] + + def test_inequality(self): + """Test tensor element-wise inequality using torch.ne().""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected element-wise inequality + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_neg_test.py b/tests/pytorch_wrapper/pt_neg_test.py new file mode 100644 index 0000000..57c66e1 --- /dev/null +++ b/tests/pytorch_wrapper/pt_neg_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_neg import PtNeg # Updated import to PtNeg + + +class TestPtNeg(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtNeg() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1, -2, 3]), torch.tensor([-1, 2, -3])), + (torch.tensor([0, 5, -7]), torch.tensor([0, -5, 7])), + (torch.ones(3, 3), torch.full((3, 3), -1.0)), # Negation of ones + (torch.zeros(2, 2), torch.zeros(2, 2)), # Negation of zeros + (torch.tensor([[5], [-10]]), torch.tensor([[-5], [10]])), # Column vector case + ] + + def test_neg(self): + """Test tensor element-wise negation using torch.neg().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected negation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_ones_test.py b/tests/pytorch_wrapper/pt_ones_test.py new file mode 100644 index 0000000..fc893af --- /dev/null +++ b/tests/pytorch_wrapper/pt_ones_test.py @@ -0,0 +1,44 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_ones import PtOnes # Importing the PtOnes class +from pytorch_wrapper.utils import DTYPE_MAPPING # Importing the dtype mapping + + +class TestPtOnes(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtOnes() + + # Tensor size to test + self.size_str = "[2, 3]" # Using a small 2D tensor size for all tests + self.expected_shape = (2, 3) + + def test_ones_tensor_all_dtypes(self): + """Test tensor creation with ones for all supported data types.""" + for data_type, torch_dtype in DTYPE_MAPPING.items(): + with self.subTest(data_type=data_type): + result, = self.node.f(self.size_str, data_type) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(self.expected_shape), f"Expected shape {self.expected_shape}, got {result.shape}") + + # Ensure result has correct dtype + self.assertEqual(result.dtype, torch_dtype, f"Expected dtype {torch_dtype}, got {result.dtype}") + + # Ensure all elements are ones + expected = torch.ones(self.expected_shape, dtype=torch_dtype) + self.assertTrue(torch.equal(result, expected), f"Tensor values do not match expected ones tensor for dtype {data_type}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_pad_test.py b/tests/pytorch_wrapper/pt_pad_test.py new file mode 100644 index 0000000..5198658 --- /dev/null +++ b/tests/pytorch_wrapper/pt_pad_test.py @@ -0,0 +1,50 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_pad import PtPad + + +class TestPtPad(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtPad() + + # Test cases: (Input tensor, Height, Width, Expected shape) + self.test_cases = [ + (torch.rand(1, 3, 16, 16), 32, 32, (1, 3, 32, 32)), # Pad 4D tensor + (torch.rand(3, 16, 16), 32, 32, (3, 32, 32)), # Pad 3D tensor + (torch.rand(1, 3, 16, 16), 16, 16, (1, 3, 16, 16)), # No padding, same size + ] + + def test_padding(self): + """Test tensor padding to specific sizes.""" + for tens, height, width, expected_shape in self.test_cases: + with self.subTest(height=height, width=width): + result, = self.node.f(tens, height, width) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result shape matches expected shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + + def test_invalid_tensor_rank(self): + """Test that an invalid tensor rank raises an error.""" + tens = torch.rand(16, 16) # 2D tensor (invalid) + with self.assertRaises(ValueError, msg="Only rank 3 or 4 tensors are supported for padding."): + self.node.f(tens, 32, 32) + + def test_padding_smaller_than_tensor(self): + """Test that padding to a smaller size raises an error.""" + tens = torch.rand(1, 3, 16, 16) + with self.assertRaises(ValueError, msg="Specified dimensions must be greater than or equal to tensor dimensions."): + self.node.f(tens, 8, 8) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_permute_test.py b/tests/pytorch_wrapper/pt_permute_test.py new file mode 100644 index 0000000..04a8e74 --- /dev/null +++ b/tests/pytorch_wrapper/pt_permute_test.py @@ -0,0 +1,59 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_permute import PtPermute + + +class TestPtPermute(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtPermute() + + # Define test cases: (input_tensor, new_axes, expected_shape) + self.test_cases = [ + # Case 1: Basic 2D permutation + (torch.ones(2, 3), "[1, 0]", (3, 2)), # (2,3) -> (3,2) + + # Case 2: Permute dimensions of a 3D tensor + (torch.ones(2, 3, 4), "[2, 0, 1]", (4, 2, 3)), # (2,3,4) -> (4,2,3) + + # Case 3: Identity permutation (no change) + (torch.ones(2, 3, 4), "[0, 1, 2]", (2, 3, 4)), # (2,3,4) -> (2,3,4) + + # Case 4: Permute 4D tensor (like a channel-first transformation) + (torch.ones(2, 3, 96, 32), "[0, 3, 1, 2]", (2, 32, 3, 96)), # (2,3,96,32) -> (2,32,3,96) + + # Case 5: Swapping first and last dimensions + (torch.ones(5, 10, 15), "[2, 1, 0]", (15, 10, 5)), # (5,10,15) -> (15,10,5) + + # Case 6: Edge case with a single-dimension tensor + (torch.ones(10,), "[0]", (10,)), # (10,) -> (10,) (No change) + + # Case 7: Larger 5D tensor permutation + (torch.ones(2, 3, 4, 5, 6), "[4, 3, 2, 1, 0]", (6, 5, 4, 3, 2)), # Reverse all dimensions + ] + + def test_permute(self): + """Test tensor permutation operation with various dimension orders.""" + for input_tensor, new_axes, expected_shape in self.test_cases: + with self.subTest(input_shape=input_tensor.shape, new_axes=new_axes): + result, = self.node.f(input_tensor, new_axes) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected_shape), f"Expected shape {expected_shape}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after permute operation") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_pow_test.py b/tests/pytorch_wrapper/pt_pow_test.py new file mode 100644 index 0000000..b6bde5f --- /dev/null +++ b/tests/pytorch_wrapper/pt_pow_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_pow import PtPow + + +class TestPtPow(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtPow() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([2.0, 3.0, 4.0]), torch.tensor([3.0, 2.0, 1.0]), torch.tensor([8.0, 9.0, 4.0])), # 2^3, 3^2, 4^1 + (torch.ones(3, 3) * 4, torch.ones(3, 3) * 2, torch.ones(3, 3) * 16), # 4^2 = 16 + (torch.full((2, 2), 5.0), torch.full((2, 2), 3.0), torch.full((2, 2), 125.0)), # 5^3 = 125 + (torch.tensor([[2.0], [3.0]]), torch.tensor([[4.0], [5.0]]), torch.tensor([[16.0], [243.0]])), # 2^4 = 16, 3^5 = 243 + ] + + def test_power(self): + """Test tensor exponentiation.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected power computation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py b/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py new file mode 100644 index 0000000..c99eda4 --- /dev/null +++ b/tests/pytorch_wrapper/pt_predict_classification_conv_model_test.py @@ -0,0 +1,68 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_conv_model import PtnConvModel +from pytorch_wrapper.ptv_image_folder_dataset import PtvImageFolderDataset +from pytorch_wrapper.ptv_transforms_resize import PtvTransformsResize +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_predict_classification_model import PtPredictClassificationModel +from pytorch_wrapper.pt_load_model import PtLoadModel + +class TestPtPrediction(unittest.TestCase): + + def setUp(self): + """Set up test instance.""" + # Transform + compose_node = PtvTransformsResize() + transform = compose_node.f(256, 256)[0] + + # Dataset + self.node = PtvImageFolderDataset() + self.dataset = self.node.f("dog_and_cat/val", + transform=transform)[0] + + + # Model + self.model_node = PtnConvModel() + self.model = self.model_node.f( + input_dim="(3, 256, 256)", + penultimate_dim=0, + output_dim=2, + channel_list="[32,64,128,256,512]", # To 128, 64, 32, 16, 8 + kernel_size_list="[3,3,3,3,3]", + padding_list="[1,1,1,1,1]", + downsample_list="[True,True,True,True,True]" + )[0] + + # Load weights + epochs = 20 + self.load_model = PtLoadModel() + self.loaded_model = self.load_model.f(self.model,f"dog_cat_{epochs}_epochs_conv.pt") + + # Instantiates evaluation node + self.predict = PtPredictClassificationModel() + + def test_prediction(self): + """ + Tests prediction. + + Note that this test requires a trained model file on the file system. + Unit test will fail if you run this for the first time. + Run the unit test for training to generate the weight, and re-run this unit test. + """ + inputs, y = next(iter(self.dataset)) + class_id_to_name_map = {0:"cat", 1:"dog"} + class_name, class_id, prob = \ + self.predict.f(self.model, inputs, class_id_to_name_map, use_gpu=True) + self.assertTrue(prob > 0.8) + self.assertEqual(class_id, 0) + self.assertEqual(class_name, "cat") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_prod_test.py b/tests/pytorch_wrapper/pt_prod_test.py new file mode 100644 index 0000000..29fd1ec --- /dev/null +++ b/tests/pytorch_wrapper/pt_prod_test.py @@ -0,0 +1,59 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_prod import PtProd + + +class TestPtProd(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtProd() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 3, 2], [2, 1, 3]]), + "0", + False, + torch.tensor([2, 3, 6]) + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "1", + True, + torch.tensor([[6], [0]]) + ), + ( + torch.tensor([[1, 3, 2], [4, 5, 6]]), + "0", + False, + torch.tensor([4, 15, 12]) + ), + ( + torch.tensor([[1, 3, 2], [4, 5, 6], [7, 8, 9]]), + "1", + False, + torch.tensor([6, 120, 504]) + ) + ] + + def test_prod(self): + """Test torch.prod behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected product values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_rand_int_test.py b/tests/pytorch_wrapper/pt_rand_int_test.py new file mode 100644 index 0000000..57c66e1 --- /dev/null +++ b/tests/pytorch_wrapper/pt_rand_int_test.py @@ -0,0 +1,40 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_neg import PtNeg # Updated import to PtNeg + + +class TestPtNeg(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtNeg() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1, -2, 3]), torch.tensor([-1, 2, -3])), + (torch.tensor([0, 5, -7]), torch.tensor([0, -5, 7])), + (torch.ones(3, 3), torch.full((3, 3), -1.0)), # Negation of ones + (torch.zeros(2, 2), torch.zeros(2, 2)), # Negation of zeros + (torch.tensor([[5], [-10]]), torch.tensor([[-5], [10]])), # Column vector case + ] + + def test_neg(self): + """Test tensor element-wise negation using torch.neg().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected negation + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_rand_test.py b/tests/pytorch_wrapper/pt_rand_test.py new file mode 100644 index 0000000..c3ef56c --- /dev/null +++ b/tests/pytorch_wrapper/pt_rand_test.py @@ -0,0 +1,46 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_rand import PtRand # Importing the PtRand class +from pytorch_wrapper.utils import DTYPE_MAPPING # Importing the dtype mapping + + +class TestPtRand(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtRand() + + # Tensor size to test + self.size_str = "[2, 3]" # Using a small 2D tensor size for all tests + self.expected_shape = (2, 3) + + # Only floating-point types are valid for torch.rand() + self.valid_dtypes = {k: v for k, v in DTYPE_MAPPING.items() if v in {torch.float32, torch.float16, torch.bfloat16, torch.float64}} + + def test_rand_tensor_all_floating_dtypes(self): + """Test random tensor creation for all supported floating-point data types.""" + for data_type, torch_dtype in self.valid_dtypes.items(): + with self.subTest(data_type=data_type): + result, = self.node.f(self.size_str, data_type) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(self.expected_shape), f"Expected shape {self.expected_shape}, got {result.shape}") + + # Ensure result has correct dtype + self.assertEqual(result.dtype, torch_dtype, f"Expected dtype {torch_dtype}, got {result.dtype}") + + # Ensure all elements are within range [0,1) + self.assertTrue(torch.all(result >= 0) and torch.all(result < 1), f"Tensor values are not in the range [0,1) for dtype {data_type}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_randn_test.py b/tests/pytorch_wrapper/pt_randn_test.py new file mode 100644 index 0000000..9022c1d --- /dev/null +++ b/tests/pytorch_wrapper/pt_randn_test.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest +import torch +import ast + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_rand_int import PtRandInt # Importing the PtRandInt class +from pytorch_wrapper.utils import DTYPE_MAPPING # Importing the dtype mapping + + +class TestPtRandInt(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtRandInt() + + # Tensor size to test + self.size_str = "[2, 3]" # Using a small 2D tensor size for all tests + self.expected_shape = (2, 3) + + # Only integer types are valid for torch.randint() + self.valid_dtypes = { + k: v for k, v in DTYPE_MAPPING.items() if v in {torch.int8, torch.int16, torch.int32, torch.int64, torch.uint8} + } + + # Define min and max values for each integer type + self.test_ranges = { + "int8": (-128, 127), + "int16": (-32768, 32767), + "int32": (-2147483648, 2147483647), + "int64": (-9223372036854775808, 9223372036854775807), + "uint8": (0, 255), + } + + def test_randint_tensor_all_integer_dtypes(self): + """Test random integer tensor creation for all supported integer data types.""" + for data_type, torch_dtype in self.valid_dtypes.items(): + min_value, max_value = self.test_ranges[data_type] + + with self.subTest(data_type=data_type, min_value=min_value, max_value=max_value): + result, = self.node.f(min_value, max_value, self.size_str, data_type) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(self.expected_shape), f"Expected shape {self.expected_shape}, got {result.shape}") + + # Ensure result has correct dtype + self.assertEqual(result.dtype, torch_dtype, f"Expected dtype {torch_dtype}, got {result.dtype}") + + # Ensure all elements are within range [min_value, max_value) + self.assertTrue(torch.all(result >= min_value), f"Tensor contains values less than min_value for dtype {data_type}") + self.assertTrue(torch.all(result < max_value), f"Tensor contains values greater than or equal to max_value for dtype {data_type}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_remainder_test.py b/tests/pytorch_wrapper/pt_remainder_test.py new file mode 100644 index 0000000..fbd17d0 --- /dev/null +++ b/tests/pytorch_wrapper/pt_remainder_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_remainder import PtRemainder + + +class TestPtRemainder(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtRemainder() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([4.0, 10.0, 18.0]), torch.tensor([3.0, 4.0, 5.0]), torch.tensor([1.0, 2.0, 3.0])), + (torch.ones(3, 3) * 7, torch.ones(3, 3) * 3, torch.ones(3, 3) * 1), # 7 % 3 = 1 + (torch.full((2, 2), 9.0), torch.full((2, 2), 2.0), torch.full((2, 2), 1.0)), # 9 % 2 = 1 + (torch.tensor([[7.0], [15.0]]), torch.tensor([[2.0], [4.0]]), torch.tensor([[1.0], [3.0]])), # Element-wise remainder + ] + + def test_remainder(self): + """Test tensor remainder computation.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected remainder + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_reshape_test.py b/tests/pytorch_wrapper/pt_reshape_test.py new file mode 100644 index 0000000..5cbcdf0 --- /dev/null +++ b/tests/pytorch_wrapper/pt_reshape_test.py @@ -0,0 +1,60 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_reshape import PtReshape # Importing the PtReshape class + + +class TestPtReshape(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtReshape() + + # Define test cases: (input_tensor, new_shape, expected_shape) + self.test_cases = [ + # Case 1: Reshape a 2D tensor + (torch.ones(2, 3), "[3, 2]", (3, 2)), # (2,3) -> (3,2) + + # Case 2: Reshape a 3D tensor + (torch.ones(2, 3, 4), "[6, 4]", (6, 4)), # (2,3,4) -> (6,4) + + # Case 3: Using -1 for automatic inference + (torch.ones(2, 3, 4), "[2, -1]", (2, 12)), # (2,3,4) -> (2,12) + (torch.ones(6, 4), "[-1, 4]", (6, 4)), # (6,4) -> (6,4) + + # Case 4: Flattening a multi-dimensional tensor + (torch.ones(2, 3, 4), "[-1]", (24,)), # (2,3,4) -> (24,) + + # Case 5: Expanding dimensions + (torch.ones(6,), "[2, 3]", (2, 3)), # (6,) -> (2,3) + + # Case 6: Reshaping a 4D tensor + (torch.ones(2, 3, 4, 5), "[6, -1, 5]", (6, 4, 5)), # (2,3,4,5) -> (6,4,5) + + # Case 7: Edge case with 1D tensor + (torch.ones(10,), "[10, 1]", (10, 1)), # (10,) -> (10,1) + ] + + def test_reshape(self): + """Test tensor reshape operation with various shape orders.""" + for input_tensor, new_shape, expected_shape in self.test_cases: + with self.subTest(input_shape=input_tensor.shape, new_shape=new_shape): + result, = self.node.f(input_tensor, new_shape) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected_shape), f"Expected shape {expected_shape}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after reshape operation") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_scatter_test.py b/tests/pytorch_wrapper/pt_scatter_test.py new file mode 100644 index 0000000..8aa2e15 --- /dev/null +++ b/tests/pytorch_wrapper/pt_scatter_test.py @@ -0,0 +1,51 @@ +import os +import sys +import unittest +import torch +import ast + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_scatter import PtScatter + + +class TestPtScatter(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtScatter() + + # Test cases: (Target Tensor, Dim, Index Tensor (as string), Source Tensor (as string), Expected Output) + self.test_cases = [ + ( + torch.tensor([ + [10, 11, 12], + [21, 22, 23], + [31, 32, 33], + ]), + 0, + "[[0, 1, 0], [1, 1, 2]]", + "[[2, 4, 6], [4, 4, 2]]", + torch.tensor([ + [2, 11, 6], + [4, 4, 23], + [31, 32, 2], + ]) + ) + ] + + def test_scatter(self): + """Test torch.scatter behavior.""" + for tens, dim, index, src, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, index=index, src=src): + result, = self.node.f(tens, dim, index, src) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_sin_test.py b/tests/pytorch_wrapper/pt_sin_test.py new file mode 100644 index 0000000..25d0b3f --- /dev/null +++ b/tests/pytorch_wrapper/pt_sin_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_sin import PtSin # Updated import to PtSin + + +class TestPtSin(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtSin() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, torch.pi / 2, torch.pi], dtype=torch.float32), torch.sin(torch.tensor([0.0, torch.pi / 2, torch.pi], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.sin(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.sin(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.sin(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_sin(self): + """Test tensor element-wise sine using torch.sin().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected sin values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_sinh_test.py b/tests/pytorch_wrapper/pt_sinh_test.py new file mode 100644 index 0000000..9e9a360 --- /dev/null +++ b/tests/pytorch_wrapper/pt_sinh_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_sinh import PtSinh # Updated import to PtSinh + + +class TestPtSinh(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtSinh() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, 1.0, -1.0], dtype=torch.float32), torch.sinh(torch.tensor([0.0, 1.0, -1.0], dtype=torch.float32))), # Edge cases + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.sinh(torch.tensor([0.5, -0.5], dtype=torch.float32))), # Common values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.sinh(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.sinh(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_sinh(self): + """Test tensor element-wise hyperbolic sine using torch.sinh().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected sinh values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_size_create_test.py b/tests/pytorch_wrapper/pt_size_create_test.py new file mode 100644 index 0000000..a229ac8 --- /dev/null +++ b/tests/pytorch_wrapper/pt_size_create_test.py @@ -0,0 +1,46 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_size_create import PtSizeCreate + +class TestPtSizeCreate(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtSizeCreate() + + # Test cases with torch.Size objects + self.test_cases = [ + # Empty size (zero-rank tensor shape) + ("[]", torch.Size([])), + # Rank 1 (single-dimension size) + ("[3]", torch.Size([3])), + # Rank 2 (matrix size) + ("[3, 4]", torch.Size([3, 4])), + # Rank 3 (3D tensor size) + ("[2, 3, 4]", torch.Size([2, 3, 4])), + # Rank 4 (higher-dimensional tensor size) + ("[1, 2, 3, 4]", torch.Size([1, 2, 3, 4])), + # Rank 5 (very high-dimensional tensor size) + ("[5, 4, 3, 2, 1]", torch.Size([5, 4, 3, 2, 1])), + ] + + def test_create_size(self): + """Test PyTorch Size creation from various structured inputs.""" + for data_str, expected_size in self.test_cases: + with self.subTest(data=data_str): + retval = self.node.f(data_str)[0] + + # Ensure the returned value is a PyTorch Size + self.assertTrue(isinstance(retval, torch.Size), f"Expected Size, got {type(retval)}") + + # Ensure the value matches expected size + self.assertEqual(retval, expected_size, f"Value mismatch for input: {data_str}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_size_test.py b/tests/pytorch_wrapper/pt_size_test.py new file mode 100644 index 0000000..c268f48 --- /dev/null +++ b/tests/pytorch_wrapper/pt_size_test.py @@ -0,0 +1,44 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_size import PtSize + +class TestPtSize(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtSize() + + # Test cases with torch.Tensor objects + self.test_cases = [ + # Rank 0 (scalar tensor) + (torch.tensor(42), torch.Size([])), + # Rank 1 (vector tensor) + (torch.tensor([1, 2, 3]), torch.Size([3])), + # Rank 2 (matrix tensor) + (torch.tensor([[1, 2], [3, 4]]), torch.Size([2, 2])), + # Rank 3 (3D tensor) + (torch.tensor([[[1], [2]], [[3], [4]]]), torch.Size([2, 2, 1])), + # Rank 4 (4D tensor) + (torch.tensor([[[[1, 2], [3, 4]]]]), torch.Size([1, 1, 2, 2])), + ] + + def test_extract_size(self): + """Test PyTorch Size extraction from tensors.""" + for tensor, expected_size in self.test_cases: + with self.subTest(data=tensor): + retval = self.node.f(tensor)[0] + + # Ensure the returned value is a PyTorch Size + self.assertTrue(isinstance(retval, torch.Size), f"Expected Size, got {type(retval)}") + + # Ensure the value matches expected size + self.assertEqual(retval, expected_size, f"Value mismatch for input: {tensor}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_size_to_numpy_test.py b/tests/pytorch_wrapper/pt_size_to_numpy_test.py new file mode 100644 index 0000000..feca5ef --- /dev/null +++ b/tests/pytorch_wrapper/pt_size_to_numpy_test.py @@ -0,0 +1,55 @@ +import os +import sys +import unittest +import torch +import numpy as np + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_size_to_numpy import PtSizeToNumpy + +class TestPtSizeToNumpy(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtSizeToNumpy() + + # Test cases with torch.Size objects + self.test_cases = [ + # Empty size (zero-rank tensor shape) + (torch.Size([]), np.array([], dtype=int)), + # Rank 1 (single-dimension size) + (torch.Size([3]), np.array([3], dtype=int)), + # Rank 2 (matrix size) + (torch.Size([3, 4]), np.array([3, 4], dtype=int)), + # Rank 3 (3D tensor size) + (torch.Size([2, 3, 4]), np.array([2, 3, 4], dtype=int)), + # Rank 4 (higher-dimensional tensor size) + (torch.Size([1, 2, 3, 4]), np.array([1, 2, 3, 4], dtype=int)), + # Rank 5 (very high-dimensional tensor size) + (torch.Size([5, 4, 3, 2, 1]), np.array([5, 4, 3, 2, 1], dtype=int)), + ] + + def test_convert_size_to_numpy(self): + """Test conversion of PyTorch Size to NumPy ndarray.""" + for torch_size, expected_array in self.test_cases: + with self.subTest(data=torch_size): + retval = self.node.f(torch_size)[0] + + # Ensure the returned value is a NumPy array + self.assertTrue(isinstance(retval, np.ndarray), f"Expected ndarray, got {type(retval)}") + + # Ensure the dtype is integer + self.assertEqual(retval.dtype, expected_array.dtype, f"Dtype mismatch for input: {torch_size}") + + # Ensure the shape matches + self.assertEqual(retval.shape, expected_array.shape, + f"Shape mismatch for input: {torch_size} (Expected {expected_array.shape}, got {retval.shape})") + + # Ensure values match + np.testing.assert_array_equal(retval, expected_array, + f"Value mismatch for input: {torch_size}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_size_to_string_test.py b/tests/pytorch_wrapper/pt_size_to_string_test.py new file mode 100644 index 0000000..69f9b54 --- /dev/null +++ b/tests/pytorch_wrapper/pt_size_to_string_test.py @@ -0,0 +1,46 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_size_to_string import PtSizeToString + +class TestPtSizeToString(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtSizeToString() + + # Test cases with torch.Size objects + self.test_cases = [ + # Empty size (zero-rank tensor shape) + (torch.Size([]), "torch.Size([])"), + # Rank 1 (single-dimension size) + (torch.Size([3]), "torch.Size([3])"), + # Rank 2 (matrix size) + (torch.Size([3, 4]), "torch.Size([3, 4])"), + # Rank 3 (3D tensor size) + (torch.Size([2, 3, 4]), "torch.Size([2, 3, 4])"), + # Rank 4 (higher-dimensional tensor size) + (torch.Size([1, 2, 3, 4]), "torch.Size([1, 2, 3, 4])"), + # Rank 5 (very high-dimensional tensor size) + (torch.Size([5, 4, 3, 2, 1]), "torch.Size([5, 4, 3, 2, 1])"), + ] + + def test_convert_size_to_string(self): + """Test conversion of PyTorch Size to string.""" + for torch_size, expected_string in self.test_cases: + with self.subTest(data=torch_size): + retval = self.node.f(torch_size)[0] + + # Ensure the returned value is a string + self.assertTrue(isinstance(retval, str), f"Expected str, got {type(retval)}") + + # Ensure the value matches expected string + self.assertEqual(retval, expected_string, f"Value mismatch for input: {torch_size}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_sqrt_test.py b/tests/pytorch_wrapper/pt_sqrt_test.py new file mode 100644 index 0000000..1254d07 --- /dev/null +++ b/tests/pytorch_wrapper/pt_sqrt_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_sqrt import PtSqrt # Updated import to PtSqrt + + +class TestPtSqrt(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtSqrt() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([1.0, 4.0, 9.0], dtype=torch.float32), torch.sqrt(torch.tensor([1.0, 4.0, 9.0], dtype=torch.float32))), # Perfect squares + (torch.tensor([0.5, 2.0, 3.0], dtype=torch.float32), torch.sqrt(torch.tensor([0.5, 2.0, 3.0], dtype=torch.float32))), # Common values + (torch.tensor([[1.5, 3.2], [4.7, 5.9]], dtype=torch.float32), torch.sqrt(torch.tensor([[1.5, 3.2], [4.7, 5.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 2.5, dtype=torch.float32), torch.sqrt(torch.full((2, 2), 2.5, dtype=torch.float32))), # Constant matrix + ] + + def test_sqrt(self): + """Test tensor element-wise square root using torch.sqrt().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected sqrt values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_squeeze_test.py b/tests/pytorch_wrapper/pt_squeeze_test.py new file mode 100644 index 0000000..a3d79f8 --- /dev/null +++ b/tests/pytorch_wrapper/pt_squeeze_test.py @@ -0,0 +1,57 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_squeeze import PtSqueeze # Importing the PtSqueeze class + + +class TestPtSqueeze(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtSqueeze() + + # Define test cases: (input_tensor, dim, expected_shape) + self.test_cases = [ + # Case 1: Squeezing a singleton dimension + (torch.ones(1, 3, 4), 0, (3, 4)), # (1,3,4) -> (3,4) + (torch.ones(2, 1, 4), 1, (2, 4)), # (2,1,4) -> (2,4) + + # Case 2: No change if dim is not a singleton + (torch.ones(2, 3, 4), 1, (2, 3, 4)), # (2,3,4) -> (2,3,4) + + # Case 3: Multiple singleton dimensions, squeezing one + (torch.ones(1, 2, 1, 4), 2, (1, 2, 4)), # (1,2,1,4) -> (1,2,4) + + # Case 4: Squeezing a negative index + (torch.ones(2, 3, 1), -1, (2, 3)), # (2,3,1) -> (2,3) + + # Case 5: Squeezing all size 1 dimensions + (torch.ones(1, 1, 1), 0, (1, 1)), # (1,1,1) -> (1,1) + (torch.ones(1, 1, 1), 1, (1, 1)), # (1,1,1) -> (1,1) + (torch.ones(1, 1, 1), 2, (1, 1)), # (1,1,1) -> (1,1) + ] + + def test_squeeze(self): + """Test tensor squeeze operation on various dimensions.""" + for input_tensor, dim, expected_shape in self.test_cases: + with self.subTest(input_shape=input_tensor.shape, dim=dim): + result, = self.node.f(input_tensor, dim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected_shape), f"Expected shape {expected_shape}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after squeeze operation") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_std_test.py b/tests/pytorch_wrapper/pt_std_test.py new file mode 100644 index 0000000..3e46908 --- /dev/null +++ b/tests/pytorch_wrapper/pt_std_test.py @@ -0,0 +1,84 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_std import PtStd + + +class TestPtStd(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtStd() + + # Define test cases with string-based dimensions + self.test_cases = [ + # Sample standard deviation (Bessel's correction) along dim=0 + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "0", + 1, + False, + torch.std(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=0, correction=1, keepdim=False) + ), + # Sample standard deviation along dim=1, with keepdim=True + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "1", + 1, + True, + torch.std(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=1, correction=1, keepdim=True) + ), + # Sample standard deviation across both axes (dim=(0,1)) + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "(0, 1)", + 1, + False, + torch.std(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=(0, 1), correction=1, keepdim=False) + ), + # Sample standard deviation across all elements (dim=None) - Entire tensor + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "", + 1, + False, + torch.std(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), correction=1, keepdim=False) + ), + # Population standard deviation along dim=0 (correction=0) + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "0", + 0, + False, + torch.std(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=0, correction=0, keepdim=False) + ), + # Population standard deviation along dim=1 with keepdim=True (correction=0) + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "1", + 0, + True, + torch.std(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=1, correction=0, keepdim=True) + ) + ] + + def test_std(self): + """Test torch.std behavior.""" + for tens, dim, correction, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, correction=correction, keepdim=keepdim): + result, = self.node.f(tens, dim, correction, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected standard deviation values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_sub_test.py b/tests/pytorch_wrapper/pt_sub_test.py new file mode 100644 index 0000000..4894c0b --- /dev/null +++ b/tests/pytorch_wrapper/pt_sub_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_sub import PtSub + + +class TestPtSub(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtSub() + + # Test cases: (Tensor A, Tensor B, Expected result) + self.test_cases = [ + (torch.tensor([5.0, 7.0, 9.0]), torch.tensor([4.0, 5.0, 6.0]), torch.tensor([1.0, 2.0, 3.0])), + (torch.full((3, 3), 2.0), torch.ones(3, 3), torch.ones(3, 3)), + (torch.ones(2, 2), torch.ones(2, 2), torch.zeros(2, 2)), + (torch.tensor([[4.0], [6.0]]), torch.tensor([[3.0], [4.0]]), torch.tensor([[1.0], [2.0]])), + ] + + def test_subtraction(self): + """Test tensor subtraction.""" + for tens_a, tens_b, expected in self.test_cases: + with self.subTest(tens_a=tens_a, tens_b=tens_b): + result, = self.node.f(tens_a, tens_b) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected difference + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_sum_test.py b/tests/pytorch_wrapper/pt_sum_test.py new file mode 100644 index 0000000..dd528ec --- /dev/null +++ b/tests/pytorch_wrapper/pt_sum_test.py @@ -0,0 +1,71 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_sum import PtSum + + +class TestPtSum(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtSum() + + # Test cases: (Tensor, Dim (as string), keepdim, Expected result) + self.test_cases = [ + ( + torch.tensor([[1, 3, 2], [2, 1, 3]]), + "0", + False, + torch.tensor([3, 4, 5]) + ), + ( + torch.tensor([[1, 3, 2], [4, 0, 5]]), + "1", + True, + torch.tensor([[6], [9]]) + ), + ( + torch.tensor([[1, 3, 2], [4, 5, 6]]), + "0", + False, + torch.tensor([5, 8, 8]) + ), + ( + torch.tensor([[1, 3, 2], [4, 5, 6], [7, 8, 9]]), + "1", + False, + torch.tensor([6, 15, 24]) + ), + ( + torch.tensor([[1, 2], [3, 4]]), + "", + False, + torch.tensor(10) # Sum of all elements + ), + ( + torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]), + "(0, 1)", + False, + torch.tensor([16, 20]) # Sum over dimensions (0, 1) + ) + ] + + def test_sum(self): + """Test torch.sum behavior.""" + for tens, dim, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, keepdim=keepdim): + result, = self.node.f(tens, dim, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected sum values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_tan_test.py b/tests/pytorch_wrapper/pt_tan_test.py new file mode 100644 index 0000000..1346872 --- /dev/null +++ b/tests/pytorch_wrapper/pt_tan_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_tan import PtTan # Updated import to PtTan + + +class TestPtTan(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtTan() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, torch.pi / 4, -torch.pi / 4], dtype=torch.float32), torch.tan(torch.tensor([0.0, torch.pi / 4, -torch.pi / 4], dtype=torch.float32))), # Common angles + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.tan(torch.tensor([0.5, -0.5], dtype=torch.float32))), # General values + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.tan(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.tan(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_tan(self): + """Test tensor element-wise tangent using torch.tan().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected tan values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_tanh_test.py b/tests/pytorch_wrapper/pt_tanh_test.py new file mode 100644 index 0000000..e2f0fc8 --- /dev/null +++ b/tests/pytorch_wrapper/pt_tanh_test.py @@ -0,0 +1,39 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_tanh import PtTanh # Updated import to PtTanh + + +class TestPtTanh(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtTanh() + + # Test cases: (Tensor A, Expected result) + self.test_cases = [ + (torch.tensor([0.0, 1.0, -1.0], dtype=torch.float32), torch.tanh(torch.tensor([0.0, 1.0, -1.0], dtype=torch.float32))), # Common values + (torch.tensor([0.5, -0.5], dtype=torch.float32), torch.tanh(torch.tensor([0.5, -0.5], dtype=torch.float32))), # General cases + (torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32), torch.tanh(torch.tensor([[0.1, -0.3], [0.7, -0.9]], dtype=torch.float32))), # 2D case + (torch.full((2, 2), 0.6, dtype=torch.float32), torch.tanh(torch.full((2, 2), 0.6, dtype=torch.float32))), # Constant matrix + ] + + def test_tanh(self): + """Test tensor element-wise hyperbolic tangent using torch.tanh().""" + for tens_a, expected in self.test_cases: + with self.subTest(tens_a=tens_a): + result, = self.node.f(tens_a) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected tanh values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_to_image_transpose_test.py b/tests/pytorch_wrapper/pt_to_image_transpose_test.py new file mode 100644 index 0000000..f6ad4f3 --- /dev/null +++ b/tests/pytorch_wrapper/pt_to_image_transpose_test.py @@ -0,0 +1,54 @@ +import os +import sys +import unittest +import torch +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.pt_to_image_transpose import PtToImageTranspose + + +class TestPtToImageTranspose(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtToImageTranspose() + + # Define test cases: (input_tensor, expected_shape) + self.test_cases = [ + # Case 1: 3D tensor (C, H, W) -> (H, W, C) + (torch.ones(3, 64, 128), (64, 128, 3)), # (3,64,128) -> (64,128,3) + + # Case 2: 4D tensor (N, C, H, W) -> (N, H, W, C) + (torch.ones(10, 3, 64, 128), (10, 64, 128, 3)), # (10,3,64,128) -> (10,64,128,3) + + # Case 3: Invalid input (2D tensor) + (torch.ones(64, 128), ValueError), # 2D tensors should raise an error + + # Case 4: Invalid input (5D tensor) + (torch.ones(10, 3, 64, 128, 2), ValueError), # 5D tensors should raise an error + ] + + def test_transpose(self): + """Test tensor to image transposition with valid and invalid cases.""" + for input_tensor, expected in self.test_cases: + with self.subTest(input_shape=input_tensor.shape): + if isinstance(expected, tuple): + # Valid cases + result, = self.node.f(input_tensor) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected), f"Expected shape {expected}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after transpose operation") + else: + # Invalid cases should raise an error + with self.assertRaises(expected): + self.node.f(input_tensor) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_to_numpy_test.py b/tests/pytorch_wrapper/pt_to_numpy_test.py new file mode 100644 index 0000000..be55c92 --- /dev/null +++ b/tests/pytorch_wrapper/pt_to_numpy_test.py @@ -0,0 +1,64 @@ +import os +import sys +import unittest +import torch +import numpy as np + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_to_numpy import PtToNumpy + +class TestPtToNumpy(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtToNumpy() + + # Test cases covering rank 0 (scalar) to rank 5 tensors + self.test_cases = [ + # Rank 0 (scalar) int32 + (torch.tensor(12345, dtype=torch.int32), np.array(12345, dtype=np.int32)), + # Rank 0 (scalar) float32 + (torch.tensor(12345.6, dtype=torch.float32), np.array(12345.6, dtype=np.float32)), + + # Rank 1 (1D array) int32 + (torch.tensor([1, 2, 3], dtype=torch.int32), np.array([1, 2, 3], dtype=np.int32)), + # Rank 1 (1D array) float32 + (torch.tensor([1.1, 2.2, 3.3], dtype=torch.float32), np.array([1.1, 2.2, 3.3], dtype=np.float32)), + + # Rank 2 (2D matrix) int32 + (torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.int32), np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)), + # Rank 2 (2D matrix) float32 + (torch.tensor([[1.1, 2.2], [3.3, 4.4]], dtype=torch.float32), np.array([[1.1, 2.2], [3.3, 4.4]], dtype=np.float32)), + + # Rank 3 (3D tensor) int32 + (torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=torch.int32), + np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], dtype=np.int32)), + # Rank 3 (3D tensor) float32 + (torch.tensor([[[1.1, 2.2], [3.3, 4.4]], [[5.5, 6.6], [7.7, 8.8]]], dtype=torch.float32), + np.array([[[1.1, 2.2], [3.3, 4.4]], [[5.5, 6.6], [7.7, 8.8]]], dtype=np.float32)), + ] + + def test_convert_tensor_to_numpy(self): + """Test conversion of PyTorch tensor to NumPy ndarray.""" + for torch_tensor, expected_array in self.test_cases: + with self.subTest(data=torch_tensor): + retval = self.node.f(torch_tensor)[0] + + # Ensure the returned value is a NumPy array + self.assertTrue(isinstance(retval, np.ndarray), f"Expected ndarray, got {type(retval)}") + + # Ensure the dtype matches + self.assertEqual(retval.dtype, expected_array.dtype, f"Dtype mismatch for input: {torch_tensor}") + + # Ensure the shape matches + self.assertEqual(retval.shape, expected_array.shape, + f"Shape mismatch for input: {torch_tensor} (Expected {expected_array.shape}, got {retval.shape})") + + # Ensure values match + np.testing.assert_array_equal(retval, expected_array, + f"Value mismatch for input: {torch_tensor}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_to_rgb_tensors_test.py b/tests/pytorch_wrapper/pt_to_rgb_tensors_test.py new file mode 100644 index 0000000..69f411a --- /dev/null +++ b/tests/pytorch_wrapper/pt_to_rgb_tensors_test.py @@ -0,0 +1,69 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_to_rgb_tensors import PtToRgbTensors + + +class TestPtToRgbTensors(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtToRgbTensors() + + # Test cases: (Input tensor, Expected R, Expected G, Expected B) + self.test_cases = [ + # 3D Tensor: [H, W, C] + ( + torch.tensor([[[255, 128, 64], [0, 127, 255]]], dtype=torch.uint8), + torch.tensor([[255, 0]], dtype=torch.uint8), + torch.tensor([[128, 127]], dtype=torch.uint8), + torch.tensor([[64, 255]], dtype=torch.uint8), + ), + + # 4D Tensor: [B, H, W, C] + ( + torch.tensor( + [[[[255, 0, 0], [0, 255, 0]], [[0, 0, 255], [255, 255, 255]]]], dtype=torch.uint8 + ), + torch.tensor([[[255, 0], [0, 255]]], dtype=torch.uint8), + torch.tensor([[[0, 255], [0, 255]]], dtype=torch.uint8), + torch.tensor([[[0, 0], [255, 255]]], dtype=torch.uint8), + ), + ] + + def test_split_rgb(self): + """Test splitting RGB tensors correctly.""" + for tensor, expected_r, expected_g, expected_b in self.test_cases: + with self.subTest(tensor_shape=tensor.shape): + r, g, b = self.node.f(tensor) + + # Ensure return values are tensors + self.assertTrue(isinstance(r, torch.Tensor), "R is not a tensor") + self.assertTrue(isinstance(g, torch.Tensor), "G is not a tensor") + self.assertTrue(isinstance(b, torch.Tensor), "B is not a tensor") + + # Ensure shapes match + self.assertEqual(r.shape, expected_r.shape, f"R shape mismatch: expected {expected_r.shape}, got {r.shape}") + self.assertEqual(g.shape, expected_g.shape, f"G shape mismatch: expected {expected_g.shape}, got {g.shape}") + self.assertEqual(b.shape, expected_b.shape, f"B shape mismatch: expected {expected_b.shape}, got {b.shape}") + + # Ensure values match + torch.testing.assert_close(r, expected_r, msg="R values mismatch") + torch.testing.assert_close(g, expected_g, msg="G values mismatch") + torch.testing.assert_close(b, expected_b, msg="B values mismatch") + + def test_invalid_input(self): + """Test error handling for non-3-channel input.""" + invalid_tensor = torch.rand(1, 512, 512, 4) # 4 channels instead of 3 + with self.assertRaises(ValueError) as context: + self.node.f(invalid_tensor) + self.assertIn("Expected a 3-channel tensor", str(context.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_train_classification_conv_model_test.py b/tests/pytorch_wrapper/pt_train_classification_conv_model_test.py new file mode 100644 index 0000000..490b602 --- /dev/null +++ b/tests/pytorch_wrapper/pt_train_classification_conv_model_test.py @@ -0,0 +1,66 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.ptn_conv_model import PtnConvModel +from pytorch_wrapper.pto_adam import PtoAdam +from pytorch_wrapper.ptv_dataset import PtvDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_train_classification_model import PtTrainClassificationModel +from pytorch_wrapper.pt_save_model import PtSaveModel + + +class TestPtTrainConvModel(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + compose_node = PtvTransformsToTensor() + transform = compose_node.f()[0] + dataset_node = PtvDataset() + dataset = dataset_node.f("FashionMNIST", + True, # download + "", # root + '{"train": True}', + transform=transform)[0] + data_loader_node = PtDataLoader() + self.train_loader = data_loader_node.f( + dataset=dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + self.model_node = PtnConvModel() + self.model = self.model_node.f( + input_dim="(1, 28, 28)", + penultimate_dim=0, + output_dim=10, + channel_list="[32,64,128,256]", + kernel_size_list="[3,3,3,3]", + padding_list="[1,1,1,1]", + downsample_list="[True,True,True,True]" + )[0] + + self.optimizer = PtoAdam().f(self.model, 0.001, 0.9, 0.999)[0] + self.trainer = PtTrainClassificationModel() + self.save_model = PtSaveModel() + + def test_1(self): + epochs = 1 + trained_model = self.trainer.f(self.model, self.train_loader, self.optimizer, + epochs, # epochs + use_gpu=True, + early_stopping=False, + early_stopping_rounds=1, + output_best_val_model=False)[0] + self.save_model.f(trained_model, f"fashion_mnist_{epochs}_epochs_conv.pt") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_train_classification_conv_model_test2.py b/tests/pytorch_wrapper/pt_train_classification_conv_model_test2.py new file mode 100644 index 0000000..c7adbf5 --- /dev/null +++ b/tests/pytorch_wrapper/pt_train_classification_conv_model_test2.py @@ -0,0 +1,76 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.ptn_conv_model import PtnConvModel +from pytorch_wrapper.pto_adam import PtoAdam +from pytorch_wrapper.ptv_image_folder_dataset import PtvImageFolderDataset +from pytorch_wrapper.ptv_transforms_resize import PtvTransformsResize +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_train_classification_model import PtTrainClassificationModel +from pytorch_wrapper.pt_save_model import PtSaveModel + + +class TestPtTrainConvModel2(unittest.TestCase): + + @unittest.skipIf(os.getenv("RUN_SKIPPED_TESTS") != "1", "Skipping unless explicitly enabled") + def setUp(self): + """Set up test instance.""" + compose_node = PtvTransformsResize() + transform = compose_node.f(256, 256)[0] + self.node = PtvImageFolderDataset() + train_dataset = self.node.f("dog_and_cat/train", + transform=transform)[0] + train_data_loader_node = PtDataLoader() + val_dataset = self.node.f("dog_and_cat/val", + transform=transform)[0] + val_data_loader_node = PtDataLoader() + self.train_loader = train_data_loader_node.f( + dataset=train_dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + self.val_loader = val_data_loader_node.f( + dataset=val_dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + self.model_node = PtnConvModel() + self.model = self.model_node.f( + input_dim="(3, 256, 256)", + penultimate_dim=0, + output_dim=2, + channel_list="[32,64,128,256,512]", # To 128, 64, 32, 16, 8 + kernel_size_list="[3,3,3,3,3]", + padding_list="[1,1,1,1,1]", + downsample_list="[True,True,True,True,True]" + )[0] + + self.optimizer = PtoAdam().f(self.model, 0.0001, 0.9, 0.999)[0] + self.trainer = PtTrainClassificationModel() + self.save_model = PtSaveModel() + + @unittest.skipIf(os.getenv("RUN_SKIPPED_TESTS") != "1", "Skipping unless explicitly enabled") + def test_1(self): + epochs = 20 + trained_model = self.trainer.f(self.model, self.train_loader, self.optimizer, + epochs, # epochs + use_gpu=True, + early_stopping=False, + early_stopping_rounds=1, + output_best_val_model=False + val_loader=self.val_loader)[0] + self.save_model.f(trained_model, f"dog_cat_{epochs}_epochs_conv.pt") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_train_classification_model_test.py b/tests/pytorch_wrapper/pt_train_classification_model_test.py new file mode 100644 index 0000000..abc8cf4 --- /dev/null +++ b/tests/pytorch_wrapper/pt_train_classification_model_test.py @@ -0,0 +1,62 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.ptn_linear_model import PtnLinearModel +from pytorch_wrapper.pto_adam import PtoAdam +from pytorch_wrapper.ptv_dataset import PtvDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor +from pytorch_wrapper.pt_data_loader import PtDataLoader +from pytorch_wrapper.pt_train_classification_model import PtTrainClassificationModel +from pytorch_wrapper.pt_save_model import PtSaveModel + + +class TestPtTrainModel(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + compose_node = PtvTransformsToTensor() + transform = compose_node.f()[0] + dataset_node = PtvDataset() + dataset = dataset_node.f("FashionMNIST", + True, # download + "", # root + '{"train": True}', + transform=transform)[0] + data_loader_node = PtDataLoader() + self.train_loader = data_loader_node.f( + dataset=dataset, + batch_size=32, + shuffle=True, + parameters='{"num_workers":1}' + )[0] + + self.model_node = PtnLinearModel() + self.model = self.model_node.f( + "784,180,42,10", # input dim for first layer, output dim for each layer + "True,True,True", # bias + 3 # num layers + )[0] + + self.optimizer = PtoAdam().f(self.model, 0.001, 0.9, 0.999)[0] + self.trainer = PtTrainClassificationModel() + self.save_model = PtSaveModel() + + def test_1(self): + epochs = 1 + trained_model = self.trainer.f(self.model, self.train_loader, self.optimizer, + epochs, # epochs + use_gpu=True, + early_stopping=False, + early_stopping_rounds=1, + output_best_val_model=False)[0] + self.save_model.f(trained_model, f"fashion_mnist_{epochs}_epochs.pt") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_train_classification_model_with_dataset_loader_test.py b/tests/pytorch_wrapper/pt_train_classification_model_with_dataset_loader_test.py new file mode 100644 index 0000000..fda2490 --- /dev/null +++ b/tests/pytorch_wrapper/pt_train_classification_model_with_dataset_loader_test.py @@ -0,0 +1,54 @@ +import os +import sys +import unittest +import torch +import torch.nn as nn + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.ptn_linear_model import PtnLinearModel +from pytorch_wrapper.pto_adam import PtoAdam +from pytorch_wrapper.ptv_dataset_loader import PtvDatasetLoader +from pytorch_wrapper.pt_train_classification_model import PtTrainClassificationModel +from pytorch_wrapper.pt_save_model import PtSaveModel + + +class TestPtTrainModelWithDatasetLoader(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.dataset_loader_node = PtvDatasetLoader() + self.train_loader = self.dataset_loader_node.f( + "MNIST", + True, # download + "", # root + batch_size=128, + shuffle=True, + dataset_parameters='{"train": True}', + load_parameters='{"num_workers":1}' + )[0] + self.model_node = PtnLinearModel() + self.model = self.model_node.f( + "784,180,42,10", # input dim for first layer, output dim for each layer + "True,True,True", # bias + 3 # num layers + )[0] + + self.optimizer = PtoAdam().f(self.model, 0.001, 0.9, 0.999)[0] + self.trainer = PtTrainClassificationModel() + self.save_model = PtSaveModel() + + def test_1(self): + epochs = 2 + trained_model = self.trainer.f(self.model, self.train_loader, self.optimizer, + epochs, # epochs + use_gpu=True, + early_stopping=False, + early_stopping_rounds=1, + output_best_val_model=False)[0] + self.save_model.f(trained_model, f"mnist_{epochs}_epochs.pt") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_unsqueeze_test.py b/tests/pytorch_wrapper/pt_unsqueeze_test.py new file mode 100644 index 0000000..d9b97a7 --- /dev/null +++ b/tests/pytorch_wrapper/pt_unsqueeze_test.py @@ -0,0 +1,57 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_unsqueeze import PtUnsqueeze # Importing the PtUnsqueeze class + + +class TestPtUnsqueeze(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtUnsqueeze() + + # Define test cases: (input_tensor, dim, expected_shape) + self.test_cases = [ + # Case 1: Adding a singleton dimension at different positions + (torch.ones(3, 4), 0, (1, 3, 4)), # (3,4) -> (1,3,4) + (torch.ones(3, 4), 1, (3, 1, 4)), # (3,4) -> (3,1,4) + (torch.ones(3, 4), 2, (3, 4, 1)), # (3,4) -> (3,4,1) + + # Case 2: Adding a singleton dimension at negative indices + (torch.ones(3, 4), -1, (3, 4, 1)), # (3,4) -> (3,4,1) + (torch.ones(3, 4), -2, (3, 1, 4)), # (3,4) -> (3,1,4) + (torch.ones(3, 4), -3, (1, 3, 4)), # (3,4) -> (1,3,4) + + # Case 3: Adding multiple dimensions one by one + (torch.ones(3,), 0, (1, 3)), # (3,) -> (1,3) + (torch.ones(3,), 1, (3, 1)), # (3,) -> (3,1) + (torch.ones(3,), -1, (3, 1)), # (3,) -> (3,1) + + # Case 4: Expanding an already multi-dimensional tensor + (torch.ones(2, 3, 4), 1, (2, 1, 3, 4)), # (2,3,4) -> (2,1,3,4) + (torch.ones(2, 3, 4), -1, (2, 3, 4, 1)), # (2,3,4) -> (2,3,4,1) + ] + + def test_unsqueeze(self): + """Test tensor unsqueeze operation on various dimensions.""" + for input_tensor, dim, expected_shape in self.test_cases: + with self.subTest(input_shape=input_tensor.shape, dim=dim): + result, = self.node.f(input_tensor, dim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected_shape), f"Expected shape {expected_shape}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after unsqueeze operation") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_var_test.py b/tests/pytorch_wrapper/pt_var_test.py new file mode 100644 index 0000000..2908cbd --- /dev/null +++ b/tests/pytorch_wrapper/pt_var_test.py @@ -0,0 +1,83 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_var import PtVar + + +class TestPtVar(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtVar() + + # Define test cases with string-based dimensions + self.test_cases = [ + # Sample variance (Bessel's correction) along dim=0 + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "0", + 1, + False, + torch.var(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=0, correction=1, keepdim=False) + ), + # Sample variance along dim=1, with keepdim=True + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "1", + 1, + True, + torch.var(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=1, correction=1, keepdim=True) + ), + # Sample variance across both axes (dim=(0,1)) + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "(0, 1)", + 1, + False, + torch.var(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=(0, 1), correction=1, keepdim=False) + ), + # Sample variance across all elements (dim=None) - Entire tensor + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "", + 1, + False, + torch.var(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), correction=1, keepdim=False) + ), + # Population variance along dim=0 (correction=0) + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "0", + 0, + False, + torch.var(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=0, correction=0, keepdim=False) + ), + # Population variance along dim=1 with keepdim=True (correction=0) + ( + torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), + "1", + 0, + True, + torch.var(torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), dim=1, correction=0, keepdim=True) + ) + ] + + def test_var(self): + """Test torch.var behavior.""" + for tens, dim, correction, keepdim, expected in self.test_cases: + with self.subTest(tens=tens, dim=dim, correction=correction, keepdim=keepdim): + result, = self.node.f(tens, dim, correction, keepdim) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected variance values + self.assertTrue(torch.allclose(result, expected, atol=1e-6), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_view_test.py b/tests/pytorch_wrapper/pt_view_test.py new file mode 100644 index 0000000..a35dc48 --- /dev/null +++ b/tests/pytorch_wrapper/pt_view_test.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_view import PtView # Importing the PtView class + + +class TestPtView(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtView() + + # Define test cases: (input_tensor, new_shape, expected_shape) + self.test_cases = [ + # Case 1: Reshape a 2D tensor + (torch.ones(2, 3), "[3, 2]", (3, 2)), # (2,3) -> (3,2) + + # Case 2: Reshape a 3D tensor + (torch.ones(2, 3, 4), "[6, 4]", (6, 4)), # (2,3,4) -> (6,4) + + # Case 3: Using -1 for automatic inference + (torch.ones(2, 3, 4), "[2, -1]", (2, 12)), # (2,3,4) -> (2,12) + (torch.ones(6, 4), "[-1, 4]", (6, 4)), # (6,4) -> (6,4) + + # Case 4: Flattening a multi-dimensional tensor + (torch.ones(2, 3, 4), "[-1]", (24,)), # (2,3,4) -> (24,) + + # Case 5: Expanding dimensions + (torch.ones(6,), "[2, 3]", (2, 3)), # (6,) -> (2,3) + + # Case 6: Reshaping a 4D tensor + (torch.ones(2, 3, 4, 5), "[6, -1, 5]", (6, 4, 5)), # (2,3,4,5) -> (6,4,5) + + # Case 7: Edge case with 1D tensor + (torch.ones(10,), "[10, 1]", (10, 1)), # (10,) -> (10,1) + ] + + def test_view(self): + """Test tensor reshape operation with various shape orders.""" + for input_tensor, new_shape, expected_shape in self.test_cases: + with self.subTest(input_shape=input_tensor.shape, new_shape=new_shape): + result, = self.node.f(input_tensor, new_shape) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(expected_shape), f"Expected shape {expected_shape}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_tensor.dtype, "Dtype mismatch after view operation") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_where_test.py b/tests/pytorch_wrapper/pt_where_test.py new file mode 100644 index 0000000..3f343fe --- /dev/null +++ b/tests/pytorch_wrapper/pt_where_test.py @@ -0,0 +1,59 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_where import PtWhere + + +class TestPtWhere(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtWhere() + + # Test cases: (Condition Tensor, True Tensor, False Tensor, Expected Output) + self.test_cases = [ + ( + torch.tensor([[True, False], [False, True]]), + torch.tensor([[1, 2], [3, 4]]), + torch.tensor([[5, 6], [7, 8]]), + torch.tensor([[1, 6], [7, 4]]) + ), + ( + torch.tensor([True, False, True]), + torch.tensor([10, 20, 30]), + torch.tensor([100, 200, 300]), + torch.tensor([10, 200, 30]) + ), + ( + torch.tensor([[False, False], [True, True]]), + torch.tensor([[1.5, 2.5], [3.5, 4.5]]), + torch.tensor([[5.5, 6.5], [7.5, 8.5]]), + torch.tensor([[5.5, 6.5], [3.5, 4.5]]) + ), + ( + torch.tensor([[[True, False], [False, True]], [[False, True], [True, False]]]), + torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]), + torch.tensor([[[9, 10], [11, 12]], [[13, 14], [15, 16]]]), + torch.tensor([[[1, 10], [11, 4]], [[13, 6], [7, 16]]]) + ), + ] + + def test_where(self): + """Test torch.where behavior.""" + for condition_tens, true_tens, false_tens, expected in self.test_cases: + with self.subTest(condition_tens=condition_tens, true_tens=true_tens, false_tens=false_tens): + result, = self.node.f(condition_tens, true_tens, false_tens) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result matches expected values + self.assertTrue(torch.equal(result, expected), f"Expected {expected}, got {result}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/pt_zeros_test.py b/tests/pytorch_wrapper/pt_zeros_test.py new file mode 100644 index 0000000..3cfda36 --- /dev/null +++ b/tests/pytorch_wrapper/pt_zeros_test.py @@ -0,0 +1,44 @@ +import os +import sys +import unittest +import torch + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.pt_zeros import PtZeros # Importing the PtZeros class +from pytorch_wrapper.utils import DTYPE_MAPPING # Importing the dtype mapping + + +class TestPtZeros(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = PtZeros() + + # Tensor size to test + self.size_str = "[2, 3]" # Using a small 2D tensor size for all tests + self.expected_shape = (2, 3) + + def test_zeros_tensor_all_dtypes(self): + """Test tensor creation with zeros for all supported data types.""" + for data_type, torch_dtype in DTYPE_MAPPING.items(): + with self.subTest(data_type=data_type): + result, = self.node.f(self.size_str, data_type) + + # Ensure return value is a tensor + self.assertTrue(isinstance(result, torch.Tensor), "Output is not a tensor") + + # Ensure result has correct shape + self.assertEqual(result.shape, torch.Size(self.expected_shape), f"Expected shape {self.expected_shape}, got {result.shape}") + + # Ensure result has correct dtype + self.assertEqual(result.dtype, torch_dtype, f"Expected dtype {torch_dtype}, got {result.dtype}") + + # Ensure all elements are zeros + expected = torch.zeros(self.expected_shape, dtype=torch_dtype) + self.assertTrue(torch.equal(result, expected), f"Tensor values do not match expected zeros tensor for dtype {data_type}") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptn_conv_model_def_test.py b/tests/pytorch_wrapper/ptn_conv_model_def_test.py new file mode 100644 index 0000000..2b3b812 --- /dev/null +++ b/tests/pytorch_wrapper/ptn_conv_model_def_test.py @@ -0,0 +1,158 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_conv_model_def import ConvModel, ConvSingleLayer + + +class TestPtnConvModelDef(unittest.TestCase): + def setUp(self): + """Setup method for initializing test conditions.""" + pass + + def test_single_layer_1_with_padding(self): + """Test ConvSingleLayer with padding=1, no downsampling.""" + model = ConvSingleLayer( + in_channels=4, + out_channels=32, + kernel_size=3, + padding=1, + downsample=False, + apply_activation=True) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 28, 28])) + + def test_single_layer_no_padding(self): + """Test ConvSingleLayer with padding=0, no downsampling.""" + model = ConvSingleLayer( + in_channels=4, + out_channels=32, + kernel_size=3, + padding=0, + downsample=False, + apply_activation=True) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 26, 26])) + + def test_single_layer_3_no_padding_downsample(self): + """Test ConvSingleLayer with padding=0 and downsampling enabled.""" + model = ConvSingleLayer( + in_channels=4, + out_channels=32, + kernel_size=3, + padding=0, + downsample=True, + apply_activation=True) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 13, 13])) + + def test_1(self): + """Test ConvModel with a single convolutional layer, checking output size.""" + model = ConvModel( + input_dim=(4, 28, 28), + penultimate_dim=0, + output_dim=10, + channel_list=[32], + kernel_size_list=[3], + padding_list=[1], + downsample_list=[False]) + + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 10])) + + def test_2(self): + """Test ConvModel intermediate convolution layers' output size.""" + model = ConvModel( + input_dim=(4, 28, 28), + penultimate_dim=0, + output_dim=10, + channel_list=[32], + kernel_size_list=[3], + padding_list=[1], + downsample_list=[False]) + + x = torch.ones(1, 4, 28, 28) + for c in model.conv_layers: + x = c(x) + out = x + self.assertEqual(out.size(), torch.Size([1, 32, 28, 28])) + + def test_3(self): + """Test ConvModel with two convolutional layers and downsampling enabled.""" + model = ConvModel( + input_dim=(4, 28, 28), + penultimate_dim=0, + output_dim=10, + channel_list=[32,64], + kernel_size_list=[3,3], + padding_list=[1,1], + downsample_list=[True,True]) + + x = torch.ones(1, 4, 28, 28) + for c in model.conv_layers: + x = c(x) + out = x + self.assertEqual(out.size(), torch.Size([1, 64, 7, 7])) + + def test_4(self): + """Test ConvModel with three convolutional layers and downsampling enabled.""" + model = ConvModel( + input_dim=(4, 28, 28), + penultimate_dim=0, + output_dim=10, + channel_list=[32,64,128], + kernel_size_list=[3,3,3], + padding_list=[1,1,1], + downsample_list=[True,True,True]) + + x = torch.ones(1, 4, 28, 28) + for c in model.conv_layers: + x = c(x) + out = x + self.assertEqual(out.size(), torch.Size([1, 128, 3, 3])) + + def test_5(self): + """Test ConvModel with four convolutional layers and downsampling enabled.""" + model = ConvModel( + input_dim=(4, 28, 28), + penultimate_dim=0, + output_dim=10, + channel_list=[32,64,128,256], + kernel_size_list=[3,3,3,3], + padding_list=[1,1,1,1], + downsample_list=[True,True,True,True]) + + x = torch.ones(1, 4, 28, 28) + for c in model.conv_layers: + x = c(x) + out = x + self.assertEqual(out.size(), torch.Size([1, 256, 1, 1])) + + def test_6(self): + """Test ConvModel with five convolutional layers, last layer without downsampling.""" + model = ConvModel( + input_dim=(4, 28, 28), + penultimate_dim=0, + output_dim=10, + channel_list=[32,64,128,256,512], + kernel_size_list=[3,3,3,3,1], + padding_list=[1,1,1,1,0], + downsample_list=[True,True,True,True,False]) + + x = torch.ones(1, 4, 28, 28) + for c in model.conv_layers: + x = c(x) + out = x + self.assertEqual(out.size(), torch.Size([1, 512, 1, 1])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptn_conv_model_test.py b/tests/pytorch_wrapper/ptn_conv_model_test.py new file mode 100644 index 0000000..9e6564e --- /dev/null +++ b/tests/pytorch_wrapper/ptn_conv_model_test.py @@ -0,0 +1,64 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_conv_model import PtnConvModel + + +class TestPtnConvModel(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.model_node = PtnConvModel() + + def test_forward(self): + model = self.model_node.f( + input_dim="(4, 28, 28)", + penultimate_dim=0, + output_dim=10, + channel_list="[32,64,128,256]", + kernel_size_list="[3,3,3,3]", + padding_list="[1,1,1,1]", + downsample_list="[True,True,True,True]" + )[0] + bs = 2 + x = torch.ones(bs, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([bs, 10])) + + def test_rank3_input(self): + model = self.model_node.f( + input_dim="(1, 28, 28)", + penultimate_dim=0, + output_dim=10, + channel_list="[32,64,128,256]", + kernel_size_list="[3,3,3,3]", + padding_list="[1,1,1,1]", + downsample_list="[True,True,True,True]" + )[0] + bs = 2 + x = torch.ones(bs, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([bs, 10])) + + def test_rank2_input(self): + model = self.model_node.f( + input_dim="(1, 28, 28)", + penultimate_dim=0, + output_dim=10, + channel_list="[32,64,128,256]", + kernel_size_list="[3,3,3,3]", + padding_list="[1,1,1,1]", + downsample_list="[True,True,True,True]" + )[0] + bs = 2 + x = torch.ones(bs, 28) + with self.assertRaises(ValueError) as context: + model(x) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptn_linear_model_test.py b/tests/pytorch_wrapper/ptn_linear_model_test.py new file mode 100644 index 0000000..124a15a --- /dev/null +++ b/tests/pytorch_wrapper/ptn_linear_model_test.py @@ -0,0 +1,29 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_linear_model import PtnLinearModel # Updated import to PtnLinear + + +class TestPtnLinearModel(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.model_node = PtnLinearModel() + self.model = self.model_node.f( + "784,180,42,10", # input dim for first layer, output dim for each layer + "True,True,True", # bias + 3 # num layers + )[0] # HINADA note: Using roughly 0.23x ratio for reduction + + def test_forward(self): + x = torch.ones(4, 28, 28) + out = self.model(x) + self.assertEqual(out.size(), torch.Size([4, 10])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptn_resnet_model_def_test.py b/tests/pytorch_wrapper/ptn_resnet_model_def_test.py new file mode 100644 index 0000000..befe066 --- /dev/null +++ b/tests/pytorch_wrapper/ptn_resnet_model_def_test.py @@ -0,0 +1,88 @@ +import os +import sys +import unittest +import torch +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_resnet_model_def import ResnetModel, SingleResnetBlock, NResnetBlocks + + +class TestPtnResnetModelDef(unittest.TestCase): + def setUp(self): + """Setup method for initializing test conditions.""" + pass + + def test_single_resnet_block_no_downsample(self): + """Test SingleResnetBlock without downsampling.""" + model = SingleResnetBlock( + in_channels=4, + out_channels=32, + kernel_size=3, + downsample=False) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 28, 28])) + + def test_single_resnet_block_downsample(self): + """Test SingleResnetBlock with downsampling.""" + model = SingleResnetBlock( + in_channels=4, + out_channels=32, + kernel_size=3, + downsample=True) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 14, 14])) + + def test_n_resnet_blocks_no_downsample(self): + """Test NResnetBlocks with multiple blocks and no downsampling.""" + model = NResnetBlocks( + num_blocks=3, + in_channels=4, + out_channels=32, + kernel_size=3, + downsample=False) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 28, 28])) + + def test_n_resnet_blocks_with_downsample(self): + """Test NResnetBlocks with multiple blocks and downsampling.""" + model = NResnetBlocks( + num_blocks=3, + in_channels=4, + out_channels=32, + kernel_size=3, + downsample=True) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 32, 14, 14])) + + def test_resnet_model_output_size(self): + """Test ResnetModel output size.""" + model = ResnetModel( + num_blocks=3, + in_channels=4, + input_height=28, + input_width=28, + output_dim=8) + x = torch.ones(1, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([1, 8])) + + def test_resnet_model_output_size2(self): + """Test ResnetModel output size.""" + model = ResnetModel( + num_blocks=3, + in_channels=3, + input_height=256, + input_width=256, + output_dim=5) + x = torch.ones(2, 3, 256, 256) + out = model(x) + self.assertEqual(out.size(), torch.Size([2, 5])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptn_resnet_model_test.py b/tests/pytorch_wrapper/ptn_resnet_model_test.py new file mode 100644 index 0000000..0ddcb2a --- /dev/null +++ b/tests/pytorch_wrapper/ptn_resnet_model_test.py @@ -0,0 +1,67 @@ +import os +import sys +import unittest +import torch + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptn_resnet_model import PtnResnetModel + + +class TestPtnResnetModel(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.model_node = PtnResnetModel() + + def test_forward(self): + model = self.model_node.f( + input_dim="(4, 28, 28)", + output_dim=10, + num_blocks=2 + )[0] + bs = 2 + x = torch.ones(bs, 4, 28, 28) + out = model(x) + self.assertEqual(out.size(), torch.Size([bs, 10])) + + def test_forward2(self): + model = self.model_node.f( + input_dim="(3, 256, 256)", + output_dim=4, + num_blocks=2 + )[0] + bs = 4 + x = torch.ones(bs, 3, 256, 256) + out = model(x) + self.assertEqual(out.size(), torch.Size([bs, 4])) + + def test_invalid_input_shape(self): + model = self.model_node.f( + input_dim="(3, 32, 32)", + output_dim=10, + num_blocks=2 + )[0] + bs = 2 + x = torch.ones(bs, 32, 32) # Missing channel dimension + with self.assertRaises(RuntimeError) as context: + model(x) + + def test_invalid_output_dim(self): + with self.assertRaises(ValueError): + self.model_node.f( + input_dim="(3, 28, 28)", + output_dim=0, # Invalid output_dim + num_blocks=2 + ) + + def test_invalid_num_blocks(self): + with self.assertRaises(ValueError): + self.model_node.f( + input_dim="(3, 28, 28)", + output_dim=10, + num_blocks=0 # Invalid num_blocks + ) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptv_dataset_loader_test.py b/tests/pytorch_wrapper/ptv_dataset_loader_test.py new file mode 100644 index 0000000..0d01690 --- /dev/null +++ b/tests/pytorch_wrapper/ptv_dataset_loader_test.py @@ -0,0 +1,34 @@ +import os +import sys +import unittest +import torch +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptv_dataset_loader import PtvDatasetLoader + +class TestPtDataLoader(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.dataset_loader_node = PtvDatasetLoader() + + def test1(self): + """Tests downloading data and checks dimension""" + bs = 2 + train_data = self.dataset_loader_node.f( + "MNIST", + True, # download + "", # root + batch_size=bs, + shuffle=True, + dataset_parameters='{"train": True}', + load_parameters='{"num_workers":1}' + )[0] + + x, y = next(iter(train_data)) + self.assertEqual(x.size(), torch.Size([bs, 1, 28, 28])) + self.assertEqual(y.size(), torch.Size([bs])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptv_dataset_test.py b/tests/pytorch_wrapper/ptv_dataset_test.py new file mode 100644 index 0000000..3eeffba --- /dev/null +++ b/tests/pytorch_wrapper/ptv_dataset_test.py @@ -0,0 +1,36 @@ +import os +import sys +import unittest + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptv_dataset import PtvDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor + +class TestPtvDataset(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtvDataset() + self.compose_node = PtvTransformsToTensor() + + def test_no_transform(self): + """Tests downloading without transform""" + self.node.f("MNIST", + True, # download + "", # root + '{"train": True}', + transform=None) + + def test_with_transform(self): + """Tests downloading with transform""" + transform = self.compose_node.f()[0] + self.node.f("MNIST", + True, # download + "", # root + '{"train": False}', + transform=transform) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptv_image_folder_dataset_test.py b/tests/pytorch_wrapper/ptv_image_folder_dataset_test.py new file mode 100644 index 0000000..bc9de3b --- /dev/null +++ b/tests/pytorch_wrapper/ptv_image_folder_dataset_test.py @@ -0,0 +1,47 @@ +""" +This unit test requires the dog_and_cat dataset to be set up under +project_root/datasets/. +""" +import os +import sys +import unittest + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptv_image_folder_dataset import PtvImageFolderDataset +from pytorch_wrapper.ptv_transforms_to_tensor import PtvTransformsToTensor + + +class TestPtvImageDataset(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PtvImageFolderDataset() + self.compose_node = PtvTransformsToTensor() + + def test_with_tensor_transform(self): + """Tests downloading with transform""" + transform = self.compose_node.f()[0] + dc = self.node.f("dog_and_cat/train", + transform=transform)[0] + expected_classes = ["cats", "dogs"] + expected_dict = [("cats", 0), ("dogs", 1)] + for i, c in enumerate(dc.classes): + self.assertEqual(c, expected_classes[i]) + + for i, (k, w) in enumerate(dc.class_to_idx.items()): + self.assertEqual(k, expected_dict[i][0]) + self.assertEqual(w, expected_dict[i][1]) + + # Read data + data = next(iter(dc)) + + # Check data shape + self.assertEqual(data[0].size(), (3, 512, 512)) + + # Check class + self.assertTrue(data[1] == 0) # cat + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/ptv_transforms_resize_test.py b/tests/pytorch_wrapper/ptv_transforms_resize_test.py new file mode 100644 index 0000000..ca6d386 --- /dev/null +++ b/tests/pytorch_wrapper/ptv_transforms_resize_test.py @@ -0,0 +1,43 @@ +import os +import sys +import unittest +import torch +from PIL import Image +import numpy as np + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pytorch_wrapper.ptv_transforms_resize import PtvTransformsResize + + +class TestPtvTransformsResize(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.compose_node = PtvTransformsResize() + self.height = 128 + self.width = 128 + + def test_with_transform(self): + """Tests the transformation output.""" + transform = self.compose_node.f(self.height, self.width)[0] + self.assertTrue(callable(transform), "Transform should be callable.") + + # Create a dummy image + img = Image.fromarray(np.random.randint(0, 256, (256, 256, 3), dtype=np.uint8)) + transformed_img = transform(img) + + # Check output type + self.assertIsInstance(transformed_img, torch.Tensor, "Output should be a PyTorch tensor.") + + # Check output shape + self.assertEqual(transformed_img.shape, (3, self.height, self.width), "Output shape should match target dimensions.") + + def test_default_parameters(self): + """Tests default transform parameters.""" + transform = self.compose_node.f(256, 256)[0] + self.assertTrue(callable(transform), "Default transform should be callable.") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pytorch_wrapper/utils_test.py b/tests/pytorch_wrapper/utils_test.py new file mode 100644 index 0000000..7101006 --- /dev/null +++ b/tests/pytorch_wrapper/utils_test.py @@ -0,0 +1,54 @@ +import os +import sys +import unittest + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pytorch_wrapper.utils import str_to_dim + + +class TestStrToDim(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + pass + + def test_valid_integer(self): + """Test parsing a valid integer string.""" + self.assertEqual(str_to_dim("5"), 5) + + def test_valid_tuple(self): + """Test parsing a valid tuple string.""" + self.assertEqual(str_to_dim("(1, 2, 3)"), (1, 2, 3)) + + def test_valid_list(self): + """Test parsing a valid list string (should be converted to tuple).""" + self.assertEqual(str_to_dim("[4, 5, 6]"), (4, 5, 6)) + + def test_empty_string(self): + """Test empty string input should return None.""" + self.assertIsNone(str_to_dim("")) + + def test_invalid_string(self): + """Test invalid string input should raise ValueError.""" + with self.assertRaises(ValueError): + str_to_dim("invalid") + + def test_mixed_type_list(self): + """Test a list with mixed types should raise ValueError.""" + with self.assertRaises(ValueError): + str_to_dim("[1, 'a', 3]") + + def test_nested_tuple(self): + """Test a nested tuple should raise ValueError.""" + with self.assertRaises(ValueError): + str_to_dim("((1, 2), 3)") + + def test_dict_input(self): + """Test a dictionary input should raise ValueError.""" + with self.assertRaises(ValueError): + str_to_dim("{'a': 1, 'b': 2}") + +if __name__ == "__main__": + unittest.main() diff --git a/web/js/ShowTextGeneric.js b/web/js/ShowTextGeneric.js new file mode 100644 index 0000000..f4ef06e --- /dev/null +++ b/web/js/ShowTextGeneric.js @@ -0,0 +1,66 @@ +/* +This code is based on +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js + +See credit/credit.md for the full license. +*/ + +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; + +// Displays input text on a node +app.registerExtension({ + name: "HowToSD.ShowTextGeneric", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (["PtShowText", "PtShowSize"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python + function populate(text) { + if (this.widgets) { + // Clear all widgets on the node + for (let i = 0; i < this.widgets.length; i++) { + // if onRemove method exists, call it. + this.widgets[i].onRemove?.(); + } + this.widgets.length = 0; + } + + const v = [...text]; // Copy the text list (e.g. text.copy()) + if (!v[0]) { + v.shift(); + } + for (const list of v) { + const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.6; + w.value = list; + } + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) { + sz[0] = this.size[0]; + } + if (sz[1] < this.size[1]) { + sz[1] = this.size[1]; + } + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text, display this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + if (this.widgets_values?.length) { + populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1)); + } + }; + } + }, +}); \ No newline at end of file