Added Pt Arange and Pt Linspace nodes.
This commit is contained in:
@@ -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()
|
||||
@@ -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 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"""
|
||||
|
||||
@@ -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,)
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user