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()