Added Pt Arange and Pt Linspace nodes.

This commit is contained in:
HowToSD
2025-03-01 12:50:55 -08:00
parent 1c4de170f9
commit 3bae9b59ff
13 changed files with 441 additions and 1 deletions
+6
View File
@@ -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",
@@ -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()
+2
View File
@@ -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 |
| --- | --- |
+25
View File
@@ -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 |
<HR>
Category: Miscellaneous
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
+25
View File
@@ -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 |
<HR>
Category: Miscellaneous
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
+35
View File
@@ -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"""
+62
View File
@@ -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,)
+62
View File
@@ -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,)
+1 -1
View File
@@ -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]
+39
View File
@@ -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()
@@ -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()
+44
View File
@@ -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()
+43
View File
@@ -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()