diff --git a/__init__.py b/__init__.py index 5246a0d..460d1af 100644 --- a/__init__.py +++ b/__init__.py @@ -124,6 +124,7 @@ from .modules.pandas_wrapper.pandas_xs import PandasXs 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 @@ -155,6 +156,7 @@ 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_le import PtLe +from .modules.pytorch_wrapper.pt_linspace import PtLinspace 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 @@ -342,6 +344,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PtAbs": PtAbs, "PtAcos": PtAcos, "PtAdd": PtAdd, + "PtArange": PtArange, "PtArgmax": PtArgmax, "PtArgmin": PtArgmin, "PtAsin": PtAsin, @@ -373,6 +376,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PtIndexSelect": PtIndexSelect, "PtIntCreate": PtIntCreate, "PtLe": PtLe, + "PtLinspace": PtLinspace, "PtLog": PtLog, "PtLogicalAnd": PtLogicalAnd, "PtLogicalNot": PtLogicalNot, @@ -563,6 +567,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PtAbs": "Pt Abs", "PtAcos": "Pt Acos", "PtAdd": "Pt Add", + "PtArange": "Pt Arange", "PtArgmax": "Pt Argmax", "PtArgmin": "Pt Argmin", "PtAsin": "Pt Asin", @@ -594,6 +599,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PtIndexSelect": "Pt Index Select", "PtIntCreate": "Pt Int Create", "PtLe": "Pt Le", + "PtLinspace": "Pt Linspace", "PtLog": "Pt Log", "PtLogicalAnd": "Pt Logical And", "PtLogicalNot": "Pt Logical Not", 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/docs/reference/node_reference.md b/docs/reference/node_reference.md index 4355b41..22bd52f 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -123,7 +123,9 @@ ## Miscellaneous | Node | Description | | --- | --- | +| [Pt Arange](pt_arange.md) | Creates a PyTorch tensor using `torch.arange` with the specified start, end, and step values. | | [Pt Flatten](pt_flatten.md) | Flattens a PyTorch tensor into a 1D tensor. | +| [Pt Linspace](pt_linspace.md) | Creates a PyTorch tensor using `torch.arange` with the specified start, end, and steps values. | ## Numpy | Node | Description | | --- | --- | diff --git a/docs/reference/pt_arange.md b/docs/reference/pt_arange.md new file mode 100644 index 0000000..6fd6fd9 --- /dev/null +++ b/docs/reference/pt_arange.md @@ -0,0 +1,25 @@ +# 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. + +Category: PyTorch wrapper - Tensor creation + +## Input +| Name | Data type | +|---|---| +| start | String | +| end | String | +| step | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: Miscellaneous + +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..09ed1e7 --- /dev/null +++ b/docs/reference/pt_linspace.md @@ -0,0 +1,25 @@ +# Pt Linspace +Creates a PyTorch tensor using `torch.arange` 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 + +## Input +| Name | Data type | +|---|---| +| start | String | +| end | String | +| steps | String | +| data_type | | + +## Output +| Data type | +|---| +| Tensor | + +
+Category: Miscellaneous + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/modules/cda/utils.py b/modules/cda/utils.py index 9979c3b..d6b9bcc 100644 --- a/modules/cda/utils.py +++ b/modules/cda/utils.py @@ -1,4 +1,39 @@ from typing import Dict, Any +from typing import Optional, Tuple, Union + +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 python_dict_first_value_len(d: Dict[Any, Any]) -> int: r""" diff --git a/modules/pytorch_wrapper/pt_arange.py b/modules/pytorch_wrapper/pt_arange.py new file mode 100644 index 0000000..405b90e --- /dev/null +++ b/modules/pytorch_wrapper/pt_arange.py @@ -0,0 +1,62 @@ +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) +from cda.utils import str_to_number, str_to_number_with_default + + +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_linspace.py b/modules/pytorch_wrapper/pt_linspace.py new file mode 100644 index 0000000..b0f4c59 --- /dev/null +++ b/modules/pytorch_wrapper/pt_linspace.py @@ -0,0 +1,62 @@ +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) +from cda.utils import str_to_number, str_to_number_with_default + + +class PtLinspace: + """ + Creates a PyTorch tensor using `torch.arange` 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/pyproject.toml b/pyproject.toml index af6d9c7..f100b04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "data-analysis" description = "Data analysis custom modules for ComfyUI - Use Pandas & Matplotlib within ComfyUI" -version = "1.4.4" +version = "1.4.5" license = {file = "LICENSE"} [project.urls] diff --git a/tests/cda/utils_str_to_number_test.py b/tests/cda/utils_str_to_number_test.py new file mode 100644 index 0000000..2d8794f --- /dev/null +++ b/tests/cda/utils_str_to_number_test.py @@ -0,0 +1,39 @@ +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 cda.utils import str_to_number + +class TestUtils(unittest.TestCase): + def test_str_to_number(self): + # Test integer conversion + self.assertEqual(str_to_number("42"), 42) + + # Test float conversion + self.assertEqual(str_to_number("3.14"), 3.14) + + # Test negative integer + self.assertEqual(str_to_number("-10"), -10) + + # Test negative float + self.assertEqual(str_to_number("-2.71"), -2.71) + + # Test zero + self.assertEqual(str_to_number("0"), 0) + self.assertEqual(str_to_number("0.0"), 0.0) + + # Test invalid input (empty string) + with self.assertRaises(ValueError): + str_to_number("") + + # Test invalid input (non-numeric string) + with self.assertRaises(ValueError): + str_to_number("abc") + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/cda/utils_str_to_number_with_default_test.py b/tests/cda/utils_str_to_number_with_default_test.py new file mode 100644 index 0000000..2ca905c --- /dev/null +++ b/tests/cda/utils_str_to_number_with_default_test.py @@ -0,0 +1,34 @@ +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 cda.utils import str_to_number_with_default + +class TestUtils(unittest.TestCase): + def test_str_to_number_with_default(self): + # Test integer conversion + self.assertEqual(str_to_number_with_default("42", 0), 42) + + # Test float conversion + self.assertEqual(str_to_number_with_default("3.14", 0.0), 3.14) + + # Test negative integer + self.assertEqual(str_to_number_with_default("-10", 0), -10) + + # Test negative float + self.assertEqual(str_to_number_with_default("-2.71", 0.0), -2.71) + + # Test zero + self.assertEqual(str_to_number_with_default("0", 1), 0) + self.assertEqual(str_to_number_with_default("0.0", 1.0), 0.0) + + # Test empty string returning default value + self.assertEqual(str_to_number_with_default("", 42), 42) + self.assertEqual(str_to_number_with_default("", -3.5), -3.5) + +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_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()