procfunc 0.30.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. procfunc/__init__.py +87 -0
  2. procfunc/color.py +57 -0
  3. procfunc/compute_graph/__init__.py +28 -0
  4. procfunc/compute_graph/compute_graph.py +115 -0
  5. procfunc/compute_graph/node.py +200 -0
  6. procfunc/compute_graph/operators_info.py +92 -0
  7. procfunc/compute_graph/proxy.py +173 -0
  8. procfunc/compute_graph/util.py +282 -0
  9. procfunc/context.py +115 -0
  10. procfunc/control.py +174 -0
  11. procfunc/nodes/__init__.py +66 -0
  12. procfunc/nodes/bindings_util.py +196 -0
  13. procfunc/nodes/bpy_node_info.py +280 -0
  14. procfunc/nodes/compositor.py +2242 -0
  15. procfunc/nodes/execute/construct_nodes.py +571 -0
  16. procfunc/nodes/execute/construct_special_cases.py +246 -0
  17. procfunc/nodes/execute/execute.py +548 -0
  18. procfunc/nodes/execute/infer_runtime_data_type.py +195 -0
  19. procfunc/nodes/execute/util.py +247 -0
  20. procfunc/nodes/func.py +1417 -0
  21. procfunc/nodes/geo.py +4240 -0
  22. procfunc/nodes/manifest.json +8769 -0
  23. procfunc/nodes/math.py +644 -0
  24. procfunc/nodes/node_function.py +160 -0
  25. procfunc/nodes/shader.py +2359 -0
  26. procfunc/nodes/types.py +347 -0
  27. procfunc/ops/__init__.py +35 -0
  28. procfunc/ops/_util.py +275 -0
  29. procfunc/ops/addons.py +59 -0
  30. procfunc/ops/attr.py +426 -0
  31. procfunc/ops/collection.py +90 -0
  32. procfunc/ops/curve.py +18 -0
  33. procfunc/ops/file.py +126 -0
  34. procfunc/ops/manifest.json +39149 -0
  35. procfunc/ops/mesh.py +1510 -0
  36. procfunc/ops/modifier.py +603 -0
  37. procfunc/ops/object.py +258 -0
  38. procfunc/ops/primitives/__init__.py +31 -0
  39. procfunc/ops/primitives/camera.py +45 -0
  40. procfunc/ops/primitives/curve.py +71 -0
  41. procfunc/ops/primitives/light.py +114 -0
  42. procfunc/ops/primitives/mesh.py +358 -0
  43. procfunc/ops/uv.py +271 -0
  44. procfunc/random.py +247 -0
  45. procfunc/tracer/__init__.py +43 -0
  46. procfunc/tracer/decorator.py +121 -0
  47. procfunc/tracer/patch.py +494 -0
  48. procfunc/tracer/proxy.py +127 -0
  49. procfunc/tracer/trace.py +222 -0
  50. procfunc/transforms/__init__.py +49 -0
  51. procfunc/transforms/cleanup.py +214 -0
  52. procfunc/transforms/convert.py +20 -0
  53. procfunc/transforms/distribution.py +191 -0
  54. procfunc/transforms/extract_materials.py +116 -0
  55. procfunc/transforms/infer_distribution.py +326 -0
  56. procfunc/transforms/parameters.py +15 -0
  57. procfunc/transforms/util.py +35 -0
  58. procfunc/transpiler/__init__.py +24 -0
  59. procfunc/transpiler/bpy_to_computegraph.py +1348 -0
  60. procfunc/transpiler/codegen.py +919 -0
  61. procfunc/transpiler/identifiers.py +595 -0
  62. procfunc/transpiler/main.py +299 -0
  63. procfunc/types.py +380 -0
  64. procfunc/util/__init__.py +0 -0
  65. procfunc/util/bpy_info.py +145 -0
  66. procfunc/util/camera.py +0 -0
  67. procfunc/util/keyframe.py +70 -0
  68. procfunc/util/log.py +96 -0
  69. procfunc/util/manifest.py +121 -0
  70. procfunc/util/pytree.py +343 -0
  71. procfunc/util/teardown.py +37 -0
  72. procfunc-0.30.0.dist-info/METADATA +120 -0
  73. procfunc-0.30.0.dist-info/RECORD +76 -0
  74. procfunc-0.30.0.dist-info/WHEEL +5 -0
  75. procfunc-0.30.0.dist-info/licenses/LICENSE.md +11 -0
  76. procfunc-0.30.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,595 @@
1
+ import logging
2
+ import re
3
+ from collections import Counter, defaultdict
4
+ from typing import Iterable, Literal, TypeVar
5
+
6
+ import bpy
7
+
8
+ from procfunc import compute_graph as cg
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def pascal_to_snake(name: str) -> str:
14
+ s = re.sub(r"[./\-\s]+", "_", name or "")
15
+ s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)
16
+ s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
17
+ return "_".join(part.lower() for part in s.split("_") if part)
18
+
19
+
20
+ def snake_to_pascal(name: str) -> str:
21
+ return "".join(part.capitalize() for part in name.split("_"))
22
+
23
+
24
+ def bpy_name_to_pythonid(name: str) -> str:
25
+ # remove .00X
26
+ # name = re.sub(r"\.\d+$", "", name)
27
+
28
+ # blender often pascal
29
+ name = pascal_to_snake(name)
30
+
31
+ # must come after pascal_to_snake or else can jam noncaps together?
32
+ name = name.replace(".", "_")
33
+ name = name.replace(" ", "_")
34
+ name = name.replace(",", "_")
35
+ name = name.replace("=", "")
36
+
37
+ name = name.lower()
38
+
39
+ name = re.sub(r"_+", "_", name).strip("_")
40
+
41
+ # move number terms to end
42
+ parts = name.split("_")
43
+ for i in range(len(parts)):
44
+ if parts[0][0].isdigit():
45
+ parts = parts[1:] + [parts[0]]
46
+
47
+ name = "_".join(parts)
48
+
49
+ return name
50
+
51
+
52
+ def is_valid_snake_identifier(name: str) -> bool:
53
+ if name == "":
54
+ return False
55
+ if name is None:
56
+ raise ValueError(f"Name {name=!r} is None")
57
+ if "." in name or " " in name:
58
+ return False
59
+ if name[0].isdigit():
60
+ return False
61
+ if name != name.lower():
62
+ return False # this is opinionated
63
+ return True
64
+
65
+
66
+ def _find_reducible(
67
+ curr_names: dict[int, str],
68
+ mode: Literal["prefix", "postfix"] = "postfix",
69
+ separator: str = ".",
70
+ existing: dict[int, str] | None = None,
71
+ limit_min_fields: int = 2,
72
+ min_str_len_reduce: int = 10,
73
+ ) -> set[int]:
74
+ new_matched_counts = defaultdict(lambda: [[], 0])
75
+
76
+ if existing is not None:
77
+ for node_id, name in existing.items():
78
+ entry = new_matched_counts[name]
79
+ entry[0].append(node_id)
80
+ entry[1] += 1
81
+
82
+ def _next_name(name: str) -> str:
83
+ return (
84
+ name[name.find(separator) + 1 :]
85
+ if mode == "postfix"
86
+ else name[name.find(separator) + 1 :]
87
+ )
88
+
89
+ def _stop_reduce(newname: str) -> bool:
90
+ return (
91
+ len(newname.split(separator)) <= limit_min_fields
92
+ or len(newname) < min_str_len_reduce
93
+ or not is_valid_snake_identifier(newname)
94
+ )
95
+
96
+ for node_id, name in curr_names.items():
97
+ matchval = _next_name(name)
98
+ if _stop_reduce(matchval):
99
+ matchval = name
100
+ if separator in name:
101
+ new_matched_counts[matchval][0].append(node_id)
102
+ new_matched_counts[matchval][1] += 1
103
+
104
+ continue_reduce_ids = set()
105
+ for name, (ids, count) in new_matched_counts.items():
106
+ if count != 1 or _stop_reduce(_next_name(name)):
107
+ continue
108
+ filtered = [
109
+ nodeid for nodeid in ids if existing is None or nodeid not in existing
110
+ ]
111
+ continue_reduce_ids.update(filtered)
112
+
113
+ return continue_reduce_ids
114
+
115
+
116
+ def duplicate_names(names: dict[int, str]) -> list[str]:
117
+ return [
118
+ (name, count) for name, count in Counter(names.values()).items() if count > 1
119
+ ]
120
+
121
+
122
+ def reduce_name_prefix_suffix(
123
+ names: dict[int, str],
124
+ mode: Literal["prefix", "postfix"] = "postfix",
125
+ separator: str = ".",
126
+ existing: dict[int, str] | None = None,
127
+ ) -> dict[int, str]:
128
+ """
129
+ remove the maximum number of `.`-separated prefix-fields from every name,
130
+ but make sure the names remain as unique as possible.
131
+ """
132
+
133
+ # logger.debug(f"{reduce_name_prefix_suffix.__name__} for {mode=} {separator=} reducing {len(names)=}")
134
+
135
+ curr_names = names.copy()
136
+
137
+ while continue_reduce := _find_reducible(
138
+ curr_names, mode=mode, separator=separator, existing=existing
139
+ ):
140
+ logger.debug(f"{reduce_name_prefix_suffix.__name__} reducing ")
141
+ for node_id in continue_reduce:
142
+ # TODO: this is not correct, we need to find the longest prefix that is shared by all nodes in the set.
143
+ if mode == "prefix":
144
+ curr_names[node_id] = curr_names[node_id].split(separator, 1)[1]
145
+ else:
146
+ raise NotImplementedError(f"Unsupported mode: {mode}")
147
+ curr_names[node_id] = curr_names[node_id].split(separator, 1)[0]
148
+
149
+ output_dups = duplicate_names(curr_names)
150
+ if output_dups: # and not duplicate_names(names):
151
+ raise ValueError(
152
+ f"{reduce_name_prefix_suffix.__name__} created duplicate names {output_dups} - "
153
+ f"should be impossible, please contact the developers. input names were {names.values()}"
154
+ )
155
+
156
+ return curr_names
157
+
158
+
159
+ def apply_panel_names_to_input_names(
160
+ node_tree: bpy.types.NodeTree,
161
+ names: dict[str, str],
162
+ only_dedup: bool = False,
163
+ ) -> dict[str, str]:
164
+ """
165
+ apply the names of the panel sockets to the input names
166
+
167
+ Args:
168
+ node_tree: the node tree to apply the panel names to
169
+ names: the input names to apply the panel names to
170
+ only_dedup: If True, only add the panel name if there are multiple sockets with the same name
171
+ """
172
+
173
+ panel_sockets = [
174
+ socket
175
+ for socket in node_tree.interface.items_tree.values()
176
+ if socket.item_type == "PANEL"
177
+ ]
178
+
179
+ if len(panel_sockets) == 0:
180
+ return names
181
+
182
+ if logger.isEnabledFor(logging.DEBUG):
183
+ socketnames = [socket.name for socket in panel_sockets]
184
+ logger.debug(
185
+ f"apply_panel_names_to_input_names on {node_tree.name=} {only_dedup=} found panel sockets {socketnames=}"
186
+ )
187
+
188
+ basename_counts = defaultdict(lambda: 0)
189
+ for socket in node_tree.interface.items_tree.values():
190
+ if socket.item_type == "PANEL":
191
+ continue
192
+ basename_counts[socket.name] += 1
193
+
194
+ seen_panel_members = set()
195
+ for panel_socket in panel_sockets:
196
+ for socket in panel_socket.interface_items.values():
197
+ if socket.identifier in seen_panel_members:
198
+ raise ValueError(
199
+ f"Panel socket {socket.identifier=} {socket.name=} appeared in multiple panels. "
200
+ "Contact the developers if this is needed."
201
+ )
202
+ seen_panel_members.add(socket.identifier)
203
+
204
+ # we will leave .-separation for now so that we can try to dedup unnecessary ones
205
+ panel_name = bpy_name_to_pythonid(panel_socket.name)
206
+ socket_name = bpy_name_to_pythonid(names[socket.identifier])
207
+ if only_dedup and basename_counts[socket.name] == 1:
208
+ names[socket.identifier] = socket_name
209
+ else:
210
+ names[socket.identifier] = panel_name + "." + socket_name
211
+
212
+ # any . that werent removed must now become _ to be valid python identifiers
213
+ for k, v in names.items():
214
+ names[k] = v.replace(".", "_")
215
+
216
+ return names
217
+
218
+
219
+ TKey = TypeVar("TKey")
220
+
221
+
222
+ def dedup_names_with_suffix(
223
+ names: dict[TKey, str],
224
+ existing: dict[TKey, str] | None = None,
225
+ separator: str = ".",
226
+ order: Iterable[TKey] | None = None,
227
+ first_use_suffix: bool = False,
228
+ ) -> dict[TKey, str]:
229
+ seen_counts: dict[str, int] = {}
230
+ result_names: dict[TKey, str] = {}
231
+
232
+ for nid, name in names.items():
233
+ parts = name.split(separator)
234
+ if len(parts) > 1 and parts[-1].isdigit():
235
+ newname = separator.join(parts[:-1])
236
+ names[nid] = newname
237
+
238
+ total_counts = Counter(names.values())
239
+
240
+ if existing is not None:
241
+ for name in existing.values():
242
+ if not isinstance(name, str):
243
+ continue
244
+ seen_counts[name] = 1
245
+ if "." in name:
246
+ seen_counts[name.split(".")[0]] = 1
247
+
248
+ if order is None:
249
+ order = list(names.keys())
250
+
251
+ for node_id in order:
252
+ if node_id not in names:
253
+ continue
254
+ if existing and node_id in existing:
255
+ continue
256
+ orig_name = names[node_id]
257
+
258
+ count = seen_counts.get(orig_name, 0)
259
+ if count == 0 and not (first_use_suffix and total_counts[orig_name] > 1):
260
+ result_names[node_id] = orig_name
261
+ else:
262
+ newname = f"{orig_name}{separator}{count}"
263
+ while newname in seen_counts:
264
+ count += 1
265
+ newname = f"{orig_name}{separator}{count}"
266
+ seen_counts[newname] = count + 1
267
+ result_names[node_id] = newname
268
+
269
+ seen_counts[orig_name] = count + 1
270
+
271
+ if dups := duplicate_names(result_names):
272
+ raise ValueError(
273
+ f"{dedup_names_with_suffix.__name__} created duplicate names {dups} - "
274
+ "should be impossible, please contact the developers. "
275
+ f"{result_names.values()=}"
276
+ )
277
+
278
+ return result_names
279
+
280
+
281
+ def _propogate_one_step(
282
+ child: cg.Node,
283
+ parent: cg.Node | None,
284
+ argname: str | int,
285
+ node_names_parts: dict[int, list[str]],
286
+ limit_n_fields: int | None,
287
+ usages: dict[int, list[cg.Node]],
288
+ fold_map: dict[int, bool] | None,
289
+ skip_propogate_words: list[str] | None = None,
290
+ ):
291
+ if parent is None:
292
+ assert isinstance(argname, str), (argname, parent, child)
293
+ return [argname]
294
+ elif (
295
+ fold_map is not None
296
+ and fold_map.get(id(parent), False)
297
+ and id(parent) not in node_names_parts
298
+ ):
299
+ assert isinstance(argname, str), (argname, parent, child)
300
+ return [argname]
301
+ elif id(parent) not in node_names_parts:
302
+ raise ValueError(
303
+ f"Visited {id(child)} as {argname=} of {id(parent)} {parent=} before parent was named?"
304
+ )
305
+
306
+ parent_parts = node_names_parts[id(parent)]
307
+
308
+ while len(parent_parts) > 1 and parent_parts[-1] in skip_propogate_words:
309
+ parent_parts = parent_parts[:-1]
310
+
311
+ if limit_n_fields is not None and len(parent_parts) > limit_n_fields - 1:
312
+ prefix = parent_parts[0] if parent_parts[0] != "result" else parent_parts[1]
313
+ return [prefix, argname]
314
+ elif fold_map is not None and all(
315
+ fold_map.get(id(usage_parent), False) for usage_parent in usages[id(child)]
316
+ ):
317
+ # no need to have a different name from parent if that parent name is always folded / never used
318
+ return parent_parts
319
+ # elif (
320
+ # len(usages[id(child)]) == 1
321
+ # and child.kind == parent.kind
322
+ # and child.target == parent.target
323
+ # ):
324
+ # # repeatedly applying a function to the same variable can reuse the same name
325
+ # return parent_parts
326
+ else:
327
+ return parent_parts + [argname]
328
+
329
+
330
+ def propogate_names_with_parts(
331
+ graph: cg.ComputeGraph,
332
+ fixed_names: dict[int, str] | None = None,
333
+ seen_subgraphs: set[int] | None = None,
334
+ limit_n_fields: int | None = None,
335
+ fold_map: dict[int, bool] | None = None,
336
+ skip_propogate_words: list[str] | None = None,
337
+ ) -> dict[int, list[str]]:
338
+ # logger.debug(f"{propogate_names_with_parts.__name__} for {graph.name}")
339
+
340
+ node_names_parts: dict[int, list[str]] = {}
341
+ fixed_name_ids: set[int] = set()
342
+
343
+ if fixed_names is not None:
344
+ for nid, v in fixed_names.items():
345
+ node_names_parts[nid] = list(v.split("_"))
346
+ fixed_name_ids.add(nid)
347
+
348
+ for i, (name, outnode) in enumerate(graph.outputs.items()):
349
+ if name == "":
350
+ if len(graph.outputs) == 1:
351
+ name = graph.name + "_result"
352
+ name = f"result_{i}"
353
+ node_names_parts[id(outnode)] = [name]
354
+
355
+ if seen_subgraphs is None:
356
+ seen_subgraphs = set()
357
+
358
+ usages = cg.usages_per_node(graph)
359
+
360
+ for argname, parent, child in cg.traverse_breadth_first(
361
+ graph,
362
+ yield_parent=True,
363
+ yield_name=True,
364
+ ):
365
+ if parent is None:
366
+ continue
367
+
368
+ assert isinstance(parent, cg.Node), (parent, child, argname)
369
+ assert argname is not None, (parent, child, argname)
370
+
371
+ result = _propogate_one_step(
372
+ child,
373
+ parent,
374
+ argname,
375
+ node_names_parts,
376
+ limit_n_fields,
377
+ usages,
378
+ fold_map,
379
+ skip_propogate_words=skip_propogate_words,
380
+ )
381
+
382
+ resname = "_".join(result)
383
+ if not is_valid_snake_identifier(resname):
384
+ parent_name = "_".join(node_names_parts.get(id(parent), []))
385
+ logger.warning(
386
+ f"Propogate from {parent} {parent_name=} to {child=} produced invalid identifier {resname=}"
387
+ )
388
+ continue
389
+
390
+ if id(child) not in node_names_parts:
391
+ node_names_parts[id(child)] = result
392
+ elif id(child) not in fixed_name_ids:
393
+ newlen = sum(len(x) for x in result)
394
+ oldlen = sum(len(x) for x in node_names_parts[id(child)])
395
+ if newlen < oldlen:
396
+ node_names_parts[id(child)] = result
397
+
398
+ return node_names_parts
399
+
400
+
401
+ def _infill_names_propogate(
402
+ graph: cg.ComputeGraph,
403
+ node_names: dict[int, str],
404
+ fold_map: dict[int, bool],
405
+ existing: dict[int, str],
406
+ skip_propogate_words: list[str] | None = None,
407
+ ):
408
+ propogated = propogate_names_with_parts(
409
+ graph,
410
+ fixed_names=node_names,
411
+ limit_n_fields=4,
412
+ fold_map=fold_map,
413
+ skip_propogate_words=skip_propogate_words,
414
+ )
415
+
416
+ propogated_names = {
417
+ id: "_".join(parts)
418
+ for id, parts in propogated.items()
419
+ if not fold_map.get(id, False)
420
+ }
421
+
422
+ propogated_names = dedup_names_with_suffix(
423
+ propogated_names,
424
+ existing={**existing, **node_names},
425
+ separator="_",
426
+ order=reversed([id(n) for n in cg.traverse_depth_first(graph)]),
427
+ first_use_suffix=True,
428
+ )
429
+
430
+ # propogated_names = reduce_name_prefix_suffix(
431
+ # propogated_names,
432
+ # mode="prefix",
433
+ # separator="_",
434
+ # existing={**existing, **node_names},
435
+ # )
436
+
437
+ if intersection := set(propogated_names.values()).intersection(
438
+ set(node_names.values())
439
+ ):
440
+ raise ValueError(f"Propogated and node names had overlap: {intersection=}")
441
+
442
+ return propogated_names
443
+
444
+
445
+ def _name_from_functionality(node: cg.Node) -> str:
446
+ match node:
447
+ case cg.FunctionCallNode(func=func):
448
+ return func.__name__
449
+ case cg.MethodCallNode(method_name=method_name):
450
+ return method_name
451
+ case cg.SubgraphCallNode(subgraph=subgraph):
452
+ return subgraph.name
453
+ case cg.GetAttributeNode(attribute_name=attribute_name):
454
+ return attribute_name
455
+ case cg.MutatedArgumentNode():
456
+ return _name_from_functionality(node.args[0])
457
+ case cg.ConstantNode():
458
+ return "const"
459
+ case cg.InputPlaceholderNode(input_name=name):
460
+ return name if name else "input"
461
+ case _:
462
+ raise NotImplementedError(f"Unsupported node: {node}")
463
+
464
+
465
+ def _infill_names_function(
466
+ graph: cg.ComputeGraph,
467
+ node_names: dict[int, str],
468
+ fold_map: dict[int, bool],
469
+ existing: dict[int, str],
470
+ ):
471
+ result = {}
472
+ for node in cg.traverse_depth_first(graph):
473
+ if id(node) in existing:
474
+ continue
475
+ if fold_map[id(node)]:
476
+ continue
477
+ if node_names.get(id(node), None) is not None:
478
+ continue
479
+ result[id(node)] = _name_from_functionality(node)
480
+
481
+ result = dedup_names_with_suffix(
482
+ result,
483
+ existing={**existing, **node_names},
484
+ separator="_",
485
+ order=reversed([id(n) for n in cg.traverse_depth_first(graph)]),
486
+ )
487
+
488
+ if intersection := set(result.values()).intersection(set(node_names.values())):
489
+ raise ValueError(f"Function and node names had overlap: {intersection=}")
490
+
491
+ return result
492
+
493
+
494
+ def _fixed_name_for_node(
495
+ node: cg.Node,
496
+ scope_expressions: dict[int, str],
497
+ avoid_parts: list[str] = [],
498
+ ) -> str | None:
499
+ """
500
+ If `node` is significant enough to be given a name, rather than being named after
501
+ its later usage, then we will return a str name for it here
502
+ """
503
+
504
+ match node:
505
+ case cg.Node(metadata={"varname": varname}):
506
+ return varname
507
+ case cg.SubgraphCallNode(subgraph=subgraph):
508
+ return subgraph.name + "_result"
509
+ case cg.FunctionCallNode(func=func):
510
+ func_resolve = scope_expressions.get(id(func), None)
511
+ module = getattr(func, "__module__", "") or ""
512
+ if not isinstance(func_resolve, str):
513
+ return None
514
+ if not (
515
+ ".geo." in func_resolve
516
+ or ".shader." in func_resolve
517
+ or module.startswith("infinigen_v2.")
518
+ ):
519
+ return None
520
+ name = "_".join(
521
+ part for part in func.__name__.split("_") if part not in avoid_parts
522
+ )
523
+ if "." in func_resolve:
524
+ module_alias = func_resolve.rsplit(".", 1)[0]
525
+ if name == module_alias:
526
+ name = name + "_result"
527
+ return name
528
+ case _:
529
+ return None
530
+
531
+
532
+ NONDESCRIPTIVE_NODE_NAME_PARTS = [
533
+ "mesh",
534
+ "geometry",
535
+ "bsdf",
536
+ "result",
537
+ "distribution",
538
+ ]
539
+
540
+
541
+ def nodenames_from_fixed_and_infill(
542
+ graph: cg.ComputeGraph,
543
+ fold_map: dict[int, bool],
544
+ scope_expressions: dict[int, str],
545
+ avoid_parts: list[str] | None = None,
546
+ ) -> dict[int, str]:
547
+ if avoid_parts is None:
548
+ avoid_parts = NONDESCRIPTIVE_NODE_NAME_PARTS
549
+
550
+ node_names = {}
551
+
552
+ for name, output in graph.outputs.items():
553
+ if name == "":
554
+ name = _name_from_functionality(output)
555
+ if not fold_map.get(id(output), False):
556
+ node_names[id(output)] = name
557
+
558
+ for name, input in graph.inputs.items():
559
+ node_names[id(input)] = name
560
+
561
+ for node in cg.traverse_depth_first(graph):
562
+ if fold_map.get(id(node), False):
563
+ continue
564
+ if name := _fixed_name_for_node(
565
+ node, scope_expressions, avoid_parts=avoid_parts
566
+ ):
567
+ node_names[id(node)] = name
568
+
569
+ node_names = dedup_names_with_suffix(
570
+ node_names,
571
+ existing=scope_expressions,
572
+ separator="_",
573
+ )
574
+
575
+ # node_names = reduce_name_prefix_suffix(
576
+ # node_names,
577
+ # mode="prefix",
578
+ # separator="_",
579
+ # existing=scope_expressions,
580
+ # )
581
+
582
+ result = _infill_names_propogate(
583
+ graph,
584
+ node_names,
585
+ fold_map,
586
+ existing=scope_expressions,
587
+ skip_propogate_words=avoid_parts,
588
+ )
589
+ result.update(node_names)
590
+
591
+ for name in result.values():
592
+ if not is_valid_snake_identifier(name):
593
+ logger.warning(f"Invalid node name: {name}")
594
+
595
+ return result