procfunc 0.34.0__tar.gz → 0.35.0__tar.gz
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.
- {procfunc-0.34.0/src/procfunc.egg-info → procfunc-0.35.0}/PKG-INFO +1 -1
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/__init__.py +1 -1
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/codegen.py +77 -14
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/repr.py +16 -15
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/proxy.py +4 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/execute.py +1 -1
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/realize.py +31 -12
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/node_function.py +6 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/__init__.py +1 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/random.py +12 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/trace.py +16 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/bpy_to_computegraph.py +42 -5
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/main.py +5 -1
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/types.py +22 -7
- {procfunc-0.34.0 → procfunc-0.35.0/src/procfunc.egg-info}/PKG-INFO +1 -1
- procfunc-0.35.0/tests/test_codegen.py +89 -0
- procfunc-0.35.0/tests/test_random.py +73 -0
- procfunc-0.34.0/tests/test_codegen.py +0 -18
- procfunc-0.34.0/tests/test_random.py +0 -29
- {procfunc-0.34.0 → procfunc-0.35.0}/LICENSE.md +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/README.md +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/pyproject.toml +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/setup.cfg +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/cli/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/cli/main.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/identifiers.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/color.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/compute_graph.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/node.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/operators_info.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/util.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/context.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/control/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/color.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/compositor.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_nodes.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_operator.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_special_cases.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_standard.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/infer_runtime_data_type.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/util.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/func.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/geo.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/manifest.json +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/math.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/shader.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/texture.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/types.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/bindings_util.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/bpy_node_info.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/_util.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/addons.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/attr.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/collection.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/curve.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/file.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/manifest.json +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/mesh.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/modifier.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/object.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/camera.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/curve.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/light.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/mesh.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/uv.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/decorator.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/patch.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/proxy.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/cleanup.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/convert.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/distribution.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/extract_materials.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/infer_distribution.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/parameters.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/util.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/parse_default_values.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/parse_special_cases.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/__init__.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/bpy_data.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/bpy_info.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/camera.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/keyframe.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/log.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/manifest.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/pytree.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/teardown.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/SOURCES.txt +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/dependency_links.txt +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/entry_points.txt +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/requires.txt +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/top_level.txt +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_asset.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_bpy_data_cleanup.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_cli_transpile.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_codegen_matrix.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_compute_graph.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_node_function.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_ops.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_pytree.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_trace.py +0 -0
- {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_transforms.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ast
|
|
1
2
|
import dataclasses
|
|
2
3
|
import enum
|
|
3
4
|
import inspect
|
|
@@ -5,7 +6,7 @@ import itertools
|
|
|
5
6
|
import logging
|
|
6
7
|
from collections import OrderedDict, defaultdict
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Any, Callable, Generator
|
|
9
|
+
from typing import Any, Callable, Generator, get_type_hints
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
11
12
|
|
|
@@ -24,6 +25,30 @@ logger = logging.getLogger(__name__)
|
|
|
24
25
|
|
|
25
26
|
INDENT = " "
|
|
26
27
|
|
|
28
|
+
# UnaryOp included since ** binds tighter than unary minus: -2.0 ** x == -(2.0 ** x)
|
|
29
|
+
_PAREN_SENSITIVE_NODES = (ast.BinOp, ast.UnaryOp, ast.Compare, ast.BoolOp, ast.IfExp)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _has_enclosing_parens(expr: str) -> bool:
|
|
33
|
+
if not (expr.startswith("(") and expr.endswith(")")):
|
|
34
|
+
return False
|
|
35
|
+
depth = 0
|
|
36
|
+
for i, char in enumerate(expr):
|
|
37
|
+
depth += (char == "(") - (char == ")")
|
|
38
|
+
if depth == 0:
|
|
39
|
+
return i == len(expr) - 1
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _needs_parens(expr: str) -> bool:
|
|
44
|
+
if _has_enclosing_parens(expr):
|
|
45
|
+
return False
|
|
46
|
+
try:
|
|
47
|
+
parsed = ast.parse(expr, mode="eval")
|
|
48
|
+
except SyntaxError:
|
|
49
|
+
return " " in expr
|
|
50
|
+
return isinstance(parsed.body, _PAREN_SENSITIVE_NODES)
|
|
51
|
+
|
|
27
52
|
|
|
28
53
|
def indent_lines(lines: list[str], indent: str = INDENT) -> list[str]:
|
|
29
54
|
return [indent + line for line in lines]
|
|
@@ -52,7 +77,7 @@ def _repr_inp(
|
|
|
52
77
|
expr = expr[0]
|
|
53
78
|
assert isinstance(expr, str)
|
|
54
79
|
|
|
55
|
-
if
|
|
80
|
+
if extra_parens and _needs_parens(expr):
|
|
56
81
|
return f"({expr})"
|
|
57
82
|
else:
|
|
58
83
|
return expr
|
|
@@ -146,7 +171,8 @@ def _repr_function_call(
|
|
|
146
171
|
if not isinstance(target, cg.Node):
|
|
147
172
|
raise ValueError(f"Method call {node=} has non-node target {target=}")
|
|
148
173
|
func = None
|
|
149
|
-
|
|
174
|
+
target_expr = _repr_inp(target, scope_expressions, extra_parens=True)
|
|
175
|
+
func_str = f"{target_expr}.{method_name}"
|
|
150
176
|
case cg.SubgraphCallNode(subgraph=subgraph):
|
|
151
177
|
func = None
|
|
152
178
|
func_str = scope_expressions.get(id(subgraph))
|
|
@@ -176,6 +202,22 @@ def _repr_function_call(
|
|
|
176
202
|
return [f"{func_str}({', '.join(arg_reprs)})"]
|
|
177
203
|
|
|
178
204
|
|
|
205
|
+
# ==/!= only match Blender Compare for exact dtypes; float/vector stay epsilon-tolerant.
|
|
206
|
+
_EPSILON_EQUALITY_TEMPLATES = frozenset(
|
|
207
|
+
OPERATOR_TEMPLATES[op] for op in (OperatorType.EQUAL, OperatorType.NOT_EQUAL)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _is_exact_compare_operand(value: Any) -> bool:
|
|
212
|
+
if isinstance(value, (cg.Node, cg.Proxy)):
|
|
213
|
+
return False
|
|
214
|
+
if isinstance(value, str):
|
|
215
|
+
return True
|
|
216
|
+
if isinstance(value, bool):
|
|
217
|
+
return True
|
|
218
|
+
return isinstance(value, int) and not isinstance(value, float)
|
|
219
|
+
|
|
220
|
+
|
|
179
221
|
def _operator_call_operands(
|
|
180
222
|
node: cg.FunctionCallNode,
|
|
181
223
|
template: str,
|
|
@@ -196,13 +238,21 @@ def _operator_call_operands(
|
|
|
196
238
|
if any(name not in bound.arguments for name in operand_names):
|
|
197
239
|
return None
|
|
198
240
|
|
|
241
|
+
operands = [bound.arguments[name] for name in operand_names]
|
|
242
|
+
|
|
243
|
+
# a default epsilon stays tolerant unlike exact ==/!=, so require exact operands
|
|
244
|
+
if template in _EPSILON_EQUALITY_TEMPLATES and not all(
|
|
245
|
+
_is_exact_compare_operand(v) for v in operands
|
|
246
|
+
):
|
|
247
|
+
return None
|
|
248
|
+
|
|
199
249
|
for name, value in bound.arguments.items():
|
|
200
250
|
if name in operand_names:
|
|
201
251
|
continue
|
|
202
252
|
if not _kwarg_matches_default(sig, name, value):
|
|
203
253
|
return None
|
|
204
254
|
|
|
205
|
-
return
|
|
255
|
+
return operands
|
|
206
256
|
|
|
207
257
|
|
|
208
258
|
def _repr_operator_call(
|
|
@@ -244,7 +294,7 @@ def _codegen_for_node(
|
|
|
244
294
|
else:
|
|
245
295
|
return _repr_function_call(node, scope_expressions)
|
|
246
296
|
case cg.MethodCallNode() if node.method_name == "__getitem__":
|
|
247
|
-
callee_expr = _repr_inp(node.args[0], scope_expressions)
|
|
297
|
+
callee_expr = _repr_inp(node.args[0], scope_expressions, extra_parens=True)
|
|
248
298
|
idx_expr = _repr_inp(node.args[1], scope_expressions)
|
|
249
299
|
return [f"{callee_expr}[{idx_expr}]"]
|
|
250
300
|
case cg.MethodCallNode():
|
|
@@ -280,7 +330,7 @@ def _codegen_graph_inputs(
|
|
|
280
330
|
) -> list[str]:
|
|
281
331
|
args = sorted(
|
|
282
332
|
list(graph.inputs.values()),
|
|
283
|
-
key=lambda x:
|
|
333
|
+
key=lambda x: "default_value" in x.kwargs,
|
|
284
334
|
)
|
|
285
335
|
|
|
286
336
|
func_name = func_name or graph.name
|
|
@@ -305,8 +355,8 @@ def _codegen_graph_inputs(
|
|
|
305
355
|
else f"{name}"
|
|
306
356
|
)
|
|
307
357
|
|
|
308
|
-
if
|
|
309
|
-
line += f" = {repr_value(
|
|
358
|
+
if "default_value" in node.kwargs:
|
|
359
|
+
line += f" = {repr_value(node.kwargs['default_value'])}"
|
|
310
360
|
|
|
311
361
|
args_lines.append(line + ",")
|
|
312
362
|
|
|
@@ -315,18 +365,31 @@ def _codegen_graph_inputs(
|
|
|
315
365
|
return [f"def {func_name}("] + indent_lines(args_lines) + [end_statement]
|
|
316
366
|
|
|
317
367
|
|
|
368
|
+
def _namedtuple_field_type(node, annotation) -> str:
|
|
369
|
+
vt = node.metadata.get("known_value_type", None)
|
|
370
|
+
if vt is None:
|
|
371
|
+
vt = annotation
|
|
372
|
+
if vt is None:
|
|
373
|
+
return "Any"
|
|
374
|
+
try:
|
|
375
|
+
return repr_type(vt)
|
|
376
|
+
except Exception:
|
|
377
|
+
return "Any"
|
|
378
|
+
|
|
379
|
+
|
|
318
380
|
def _codegen_namedtuple_def(outputs: pytree.PyTree):
|
|
319
381
|
tupletype = outputs.toplevel_type()
|
|
382
|
+
try:
|
|
383
|
+
annotations = get_type_hints(tupletype)
|
|
384
|
+
except Exception:
|
|
385
|
+
annotations = getattr(tupletype, "__annotations__", {})
|
|
320
386
|
|
|
321
387
|
type_lines = []
|
|
322
388
|
for name, node in outputs.items():
|
|
323
389
|
if node is None:
|
|
324
390
|
continue
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
type_lines.append(f"{name}: Any")
|
|
328
|
-
else:
|
|
329
|
-
type_lines.append(f"{name}: {repr_type(vt)}")
|
|
391
|
+
field_type = _namedtuple_field_type(node, annotations.get(name))
|
|
392
|
+
type_lines.append(f"{name}: {field_type}")
|
|
330
393
|
|
|
331
394
|
return [f"class {tupletype.__name__}(NamedTuple):"] + indent_lines(type_lines)
|
|
332
395
|
|
|
@@ -840,7 +903,7 @@ def to_python(
|
|
|
840
903
|
add_line_comments: bool = False,
|
|
841
904
|
) -> str:
|
|
842
905
|
code_lines = []
|
|
843
|
-
code_lines.append("from typing import NamedTuple, Annotated")
|
|
906
|
+
code_lines.append("from typing import Any, NamedTuple, Annotated")
|
|
844
907
|
code_lines.append("import numpy as np")
|
|
845
908
|
code_lines.append("import bpy")
|
|
846
909
|
# code_lines.append("import logging; logging.basicConfig(level=logging.DEBUG)")
|
|
@@ -2,6 +2,7 @@ import dataclasses
|
|
|
2
2
|
import enum
|
|
3
3
|
import logging
|
|
4
4
|
import math
|
|
5
|
+
import types
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any, Union, get_args, get_origin
|
|
7
8
|
|
|
@@ -20,12 +21,22 @@ def repr_type(x: Any) -> str:
|
|
|
20
21
|
if isinstance(x, str):
|
|
21
22
|
return x
|
|
22
23
|
|
|
23
|
-
if x.__name__ == "NoneType":
|
|
24
|
-
return "None"
|
|
25
|
-
|
|
26
24
|
origin = get_origin(x)
|
|
27
25
|
args = get_args(x)
|
|
28
26
|
|
|
27
|
+
if origin is Union or origin is types.UnionType:
|
|
28
|
+
first_args = get_args(args[0])
|
|
29
|
+
if (
|
|
30
|
+
get_origin(args[0]) is nt.ProcNode
|
|
31
|
+
and first_args
|
|
32
|
+
and first_args[0] is args[1]
|
|
33
|
+
):
|
|
34
|
+
return f"t.SocketOrVal[{repr_type(first_args[0])}]"
|
|
35
|
+
return " | ".join([repr_type(a) for a in args])
|
|
36
|
+
|
|
37
|
+
if x.__name__ == "NoneType":
|
|
38
|
+
return "None"
|
|
39
|
+
|
|
29
40
|
if x.__name__ == "ProcNode":
|
|
30
41
|
if len(args) == 1:
|
|
31
42
|
return f"pf.ProcNode[{repr_type(args[0])}]"
|
|
@@ -42,16 +53,6 @@ def repr_type(x: Any) -> str:
|
|
|
42
53
|
if x.__module__ == "builtins":
|
|
43
54
|
return x.__name__
|
|
44
55
|
|
|
45
|
-
origin = get_origin(x)
|
|
46
|
-
args = get_args(x)
|
|
47
|
-
|
|
48
|
-
if origin is Union:
|
|
49
|
-
args_0 = get_args(args[0])
|
|
50
|
-
if get_origin(args[0]) is nt.ProcNode and args_0[0] is args[1]:
|
|
51
|
-
return f"t.SocketOrVal[{repr_type(args_0[0])}]"
|
|
52
|
-
else:
|
|
53
|
-
return " | ".join([repr_type(a) for a in args])
|
|
54
|
-
|
|
55
56
|
if getattr(x, "__module__", None) == "procfunc.nodes.types":
|
|
56
57
|
return f"t.{x.__name__}"
|
|
57
58
|
|
|
@@ -70,12 +71,12 @@ def repr_value(value: Any) -> str:
|
|
|
70
71
|
value = value.__wrapped__
|
|
71
72
|
|
|
72
73
|
if isinstance(value, cg.Proxy):
|
|
73
|
-
|
|
74
|
+
raise ValueError(
|
|
74
75
|
f"Proxy object {value} should never appear as a raw value in codegen - "
|
|
75
76
|
f"its underlying node {value.node} was not resolved to a variable"
|
|
76
77
|
)
|
|
77
78
|
if isinstance(value, nt.ProcNode):
|
|
78
|
-
|
|
79
|
+
raise ValueError(
|
|
79
80
|
f"Procnode object {value} should never be treated as a raw value in codegen"
|
|
80
81
|
)
|
|
81
82
|
|
|
@@ -15,6 +15,10 @@ class Proxy(Generic[T]):
|
|
|
15
15
|
|
|
16
16
|
node: Node
|
|
17
17
|
|
|
18
|
+
# Tell numpy to defer (ndarray + Proxy -> Proxy.__radd__) instead of probing the
|
|
19
|
+
# Proxy as an array, which would fabricate a bogus __array_struct__ via __getattr__.
|
|
20
|
+
__array_ufunc__ = None
|
|
21
|
+
|
|
18
22
|
def __repr__(self):
|
|
19
23
|
return f"Proxy({self.node!r})"
|
|
20
24
|
|
|
@@ -5,13 +5,13 @@ import bpy
|
|
|
5
5
|
from procfunc import compute_graph as cg
|
|
6
6
|
from procfunc import types as pt
|
|
7
7
|
from procfunc.nodes import types as nt
|
|
8
|
-
from procfunc.nodes.util.bpy_node_info import
|
|
8
|
+
from procfunc.nodes.util.bpy_node_info import SocketType
|
|
9
9
|
from procfunc.ops._util import modify
|
|
10
10
|
from procfunc.ops.primitives.mesh import mesh_single_vertex
|
|
11
|
-
from procfunc.util import pytree
|
|
12
11
|
from procfunc.util.bpy_info import bpy_nocollide_data_name
|
|
13
12
|
|
|
14
|
-
from .
|
|
13
|
+
from . import construct_standard
|
|
14
|
+
from .construct_nodes import construct_procnode_to_bpy, instantiate_nodegroup
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
@@ -81,11 +81,38 @@ def nodegroup_to_output(
|
|
|
81
81
|
return output_node
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def construct_outputs_to_output_node(
|
|
85
|
+
node_tree: bpy.types.NodeTree,
|
|
86
|
+
outputs: dict[str, cg.Node],
|
|
87
|
+
output_node_type: str,
|
|
88
|
+
) -> bpy.types.Node:
|
|
89
|
+
output_node = node_tree.nodes.new(output_node_type)
|
|
90
|
+
cache = {}
|
|
91
|
+
for key, node in outputs.items():
|
|
92
|
+
res = construct_procnode_to_bpy(node, node_tree, cache)
|
|
93
|
+
if isinstance(res, bpy.types.Node):
|
|
94
|
+
res = construct_standard._get_primary_output_socket(node, res)
|
|
95
|
+
construct_standard.connect_single_input(
|
|
96
|
+
node_tree, output_node.inputs[key.capitalize()], res
|
|
97
|
+
)
|
|
98
|
+
return output_node
|
|
99
|
+
|
|
100
|
+
|
|
84
101
|
def build_bpy_material(
|
|
85
102
|
surface: nt.ProcNode[nt.Shader] | None = None,
|
|
86
103
|
displacement: nt.ProcNode[pt.Vector] | None = None,
|
|
87
104
|
volume: nt.ProcNode[nt.Shader] | None = None,
|
|
88
105
|
) -> bpy.types.Material:
|
|
106
|
+
for key, val in {
|
|
107
|
+
"surface": surface,
|
|
108
|
+
"displacement": displacement,
|
|
109
|
+
"volume": volume,
|
|
110
|
+
}.items():
|
|
111
|
+
if val is not None and not isinstance(val, (nt.ProcNode, cg.Node)):
|
|
112
|
+
raise TypeError(
|
|
113
|
+
f"Material {key} must be a ProcNode or None, got {type(val)}: {val!r}"
|
|
114
|
+
)
|
|
115
|
+
|
|
89
116
|
# optimization: a constant zero displacement has no effect, so drop it and
|
|
90
117
|
# leave the output socket disconnected rather than emitting a dead subgraph
|
|
91
118
|
if displacement is not None and pt.is_zero_displacement(displacement):
|
|
@@ -107,15 +134,7 @@ def build_bpy_material(
|
|
|
107
134
|
mnt = material.node_tree
|
|
108
135
|
mnt.nodes.clear()
|
|
109
136
|
|
|
110
|
-
outputs
|
|
111
|
-
graph = cg.ComputeGraph(
|
|
112
|
-
inputs=pytree.PyTree({}),
|
|
113
|
-
outputs=outputs,
|
|
114
|
-
name="to_material",
|
|
115
|
-
metadata={},
|
|
116
|
-
)
|
|
117
|
-
body = as_nodegroup(graph, NodeGroupType.SHADER)
|
|
118
|
-
nodegroup_to_output(mnt, body, "ShaderNodeOutputMaterial", list(outputs.names()))
|
|
137
|
+
construct_outputs_to_output_node(mnt, outputs, "ShaderNodeOutputMaterial")
|
|
119
138
|
|
|
120
139
|
return material
|
|
121
140
|
|
|
@@ -144,6 +144,12 @@ def _subgraph_call_procnode(func: Callable, subgraph: cg.ComputeGraph, *args, **
|
|
|
144
144
|
def node_function(func: Callable):
|
|
145
145
|
@functools.wraps(func)
|
|
146
146
|
def node_function_wrapper(*args, **kwargs):
|
|
147
|
+
active = pf.context.globals.current_trace_level
|
|
148
|
+
if active is not None and active >= TraceLevel.NODEGROUPS.value:
|
|
149
|
+
from procfunc.tracer.trace import capture_as_function_call
|
|
150
|
+
|
|
151
|
+
return capture_as_function_call(node_function_wrapper, args, kwargs)
|
|
152
|
+
|
|
147
153
|
subgraph = _execute_procnode_func_to_computegraph(func)
|
|
148
154
|
subgraph.metadata["operations"] = [
|
|
149
155
|
(node_function, {"func": func}),
|
|
@@ -75,6 +75,7 @@ def clip_gaussian(
|
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
78
79
|
def wrap_gaussian(
|
|
79
80
|
rng: np.random.Generator,
|
|
80
81
|
mean: float,
|
|
@@ -92,6 +93,7 @@ def wrap_gaussian(
|
|
|
92
93
|
return x
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
95
97
|
def exponential(
|
|
96
98
|
rng: np.random.Generator,
|
|
97
99
|
scale: float,
|
|
@@ -99,6 +101,7 @@ def exponential(
|
|
|
99
101
|
return rng.exponential(scale)
|
|
100
102
|
|
|
101
103
|
|
|
104
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
102
105
|
def log_uniform(
|
|
103
106
|
rng: np.random.Generator,
|
|
104
107
|
low: float,
|
|
@@ -108,6 +111,7 @@ def log_uniform(
|
|
|
108
111
|
return np.exp(rng.uniform(np.log(low), np.log(high), size=size))
|
|
109
112
|
|
|
110
113
|
|
|
114
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
111
115
|
def log_normal(
|
|
112
116
|
rng: np.random.Generator,
|
|
113
117
|
mean: float,
|
|
@@ -117,6 +121,7 @@ def log_normal(
|
|
|
117
121
|
return np.exp(rng.normal(np.log(mean), std, size=size))
|
|
118
122
|
|
|
119
123
|
|
|
124
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
120
125
|
def spherical_sample(
|
|
121
126
|
rng: np.random.Generator,
|
|
122
127
|
min_elevation: float | None = None,
|
|
@@ -137,6 +142,7 @@ def spherical_sample(
|
|
|
137
142
|
)
|
|
138
143
|
|
|
139
144
|
|
|
145
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
140
146
|
def mixture_of_gaussian(
|
|
141
147
|
rng: np.random.Generator,
|
|
142
148
|
means: np.ndarray,
|
|
@@ -161,6 +167,7 @@ def mixture_of_gaussian(
|
|
|
161
167
|
return res
|
|
162
168
|
|
|
163
169
|
|
|
170
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
164
171
|
def beta(
|
|
165
172
|
rng: np.random.Generator,
|
|
166
173
|
a: float,
|
|
@@ -169,6 +176,7 @@ def beta(
|
|
|
169
176
|
return rng.beta(a, b)
|
|
170
177
|
|
|
171
178
|
|
|
179
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
172
180
|
def poisson(
|
|
173
181
|
rng: np.random.Generator,
|
|
174
182
|
lam: float,
|
|
@@ -176,6 +184,7 @@ def poisson(
|
|
|
176
184
|
return rng.poisson(lam)
|
|
177
185
|
|
|
178
186
|
|
|
187
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
179
188
|
def triangular(
|
|
180
189
|
rng: np.random.Generator,
|
|
181
190
|
low: float,
|
|
@@ -185,6 +194,7 @@ def triangular(
|
|
|
185
194
|
return rng.triangular(low, mode, high)
|
|
186
195
|
|
|
187
196
|
|
|
197
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
188
198
|
def gamma(
|
|
189
199
|
rng: np.random.Generator,
|
|
190
200
|
shape: float,
|
|
@@ -193,6 +203,7 @@ def gamma(
|
|
|
193
203
|
return rng.gamma(shape, scale)
|
|
194
204
|
|
|
195
205
|
|
|
206
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
196
207
|
def binomial(
|
|
197
208
|
rng: np.random.Generator,
|
|
198
209
|
n: int,
|
|
@@ -201,6 +212,7 @@ def binomial(
|
|
|
201
212
|
return rng.binomial(n, p)
|
|
202
213
|
|
|
203
214
|
|
|
215
|
+
@pf.tracer.primitive(allow_exec=True)
|
|
204
216
|
def geometric(
|
|
205
217
|
rng: np.random.Generator,
|
|
206
218
|
p: float,
|
|
@@ -139,6 +139,22 @@ def _map_args(
|
|
|
139
139
|
return res
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def capture_as_function_call(func: Callable, args: tuple, kwargs: dict) -> cg.Proxy:
|
|
143
|
+
"""Record a call as a single FunctionCallNode leaf without executing func's body.
|
|
144
|
+
|
|
145
|
+
The Proxy's attribute/item access becomes graph getattrs, so multi-output results
|
|
146
|
+
(e.g. a NamedTuple) work through it. Codegen re-emits this as an imported call.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def _unwrap(v):
|
|
150
|
+
return v.node if isinstance(v, cg.Proxy) else v
|
|
151
|
+
|
|
152
|
+
node_args = tuple(pytree.PyTree(a).map(_unwrap).obj() for a in args)
|
|
153
|
+
node_kwargs = {k: pytree.PyTree(v).map(_unwrap).obj() for k, v in kwargs.items()}
|
|
154
|
+
node = cg.FunctionCallNode(func=func, args=node_args, kwargs=node_kwargs)
|
|
155
|
+
return cg.Proxy(node)
|
|
156
|
+
|
|
157
|
+
|
|
142
158
|
def trace(
|
|
143
159
|
func: Callable,
|
|
144
160
|
trace_level: TraceLevel = TraceLevel.GENERATORS,
|
|
@@ -163,6 +163,36 @@ def _remove_banned_attrs(
|
|
|
163
163
|
)
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def _returns_named_tuple(func: Any) -> bool:
|
|
167
|
+
try:
|
|
168
|
+
return_type = get_type_hints(func).get("return")
|
|
169
|
+
except Exception:
|
|
170
|
+
return False
|
|
171
|
+
if return_type is None:
|
|
172
|
+
return False
|
|
173
|
+
origin = get_origin(return_type) or return_type
|
|
174
|
+
return (
|
|
175
|
+
isinstance(origin, type)
|
|
176
|
+
and issubclass(origin, tuple)
|
|
177
|
+
and hasattr(origin, "_fields")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _produces_named_outputs(node: cg.Node, link: bpy.types.NodeLink) -> bool:
|
|
182
|
+
"""Whether the parsed node yields multiple named outputs, so the downstream
|
|
183
|
+
link must select a specific one via getattr. A node's procfunc function may
|
|
184
|
+
return a namedtuple of all outputs (e.g. attribute_domain_size) even when
|
|
185
|
+
the source node's mode/component disables all but one output socket, so the
|
|
186
|
+
active-socket count alone underdetects this - prefer the function's return
|
|
187
|
+
type and fall back to the socket count when it is unknown."""
|
|
188
|
+
if isinstance(node, cg.FunctionCallNode):
|
|
189
|
+
return _returns_named_tuple(node.func)
|
|
190
|
+
if isinstance(node, cg.SubgraphCallNode):
|
|
191
|
+
out = node.subgraph.outputs.obj()
|
|
192
|
+
return isinstance(out, tuple) and hasattr(out, "_fields")
|
|
193
|
+
return len(get_active_sockets(link.from_node.outputs)) > 1
|
|
194
|
+
|
|
195
|
+
|
|
166
196
|
def _parse_getattr(
|
|
167
197
|
res: cg.Node,
|
|
168
198
|
link: bpy.types.NodeLink,
|
|
@@ -232,9 +262,8 @@ def _create_link_impl_node(
|
|
|
232
262
|
res = parse_node(node_tree, link.from_node, memo)
|
|
233
263
|
assert res is not None, link
|
|
234
264
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if len(outsockets) > 1:
|
|
265
|
+
assert len(get_active_sockets(link.from_node.outputs)) > 0
|
|
266
|
+
if _produces_named_outputs(res, link):
|
|
238
267
|
res = _parse_getattr(res, link)
|
|
239
268
|
assert res is not None, link
|
|
240
269
|
|
|
@@ -912,8 +941,10 @@ def _placeholder_for_graph_input(
|
|
|
912
941
|
varname=varname,
|
|
913
942
|
),
|
|
914
943
|
)
|
|
915
|
-
|
|
916
|
-
|
|
944
|
+
# Record the default unconditionally, even when it is None: a socket with no
|
|
945
|
+
# synthesizable value (geometry/object/collection) should still become an
|
|
946
|
+
# optional param (= None), matching v1 where every input had a default.
|
|
947
|
+
node.kwargs["default_value"] = default_value
|
|
917
948
|
|
|
918
949
|
return node
|
|
919
950
|
|
|
@@ -1261,6 +1292,12 @@ def parse_material(
|
|
|
1261
1292
|
if output_node.inputs[key].is_linked:
|
|
1262
1293
|
res = parse_link(node_tree, output_node.inputs[key].links[0], memo)
|
|
1263
1294
|
res.metadata["known_value_type"] = pf.ProcNode[expect_type]
|
|
1295
|
+
elif key == "Displacement":
|
|
1296
|
+
# unconnected displacement is a zero ProcNode, composable arithmetically
|
|
1297
|
+
res = cg.FunctionCallNode(
|
|
1298
|
+
func=pf.nodes.math.constant, args=((0.0, 0.0, 0.0),), kwargs={}
|
|
1299
|
+
)
|
|
1300
|
+
res.metadata["known_value_type"] = pf.ProcNode[pf.Vector]
|
|
1264
1301
|
else:
|
|
1265
1302
|
res = cg.ConstantNode(value=None)
|
|
1266
1303
|
res.metadata["known_value_type"] = Union[pf.ProcNode[expect_type], None]
|
|
@@ -215,7 +215,8 @@ def transpile_targets(
|
|
|
215
215
|
result_calls = []
|
|
216
216
|
for result_graph in result_graphs:
|
|
217
217
|
kwargs = {}
|
|
218
|
-
|
|
218
|
+
input_names = {name for name, _ in result_graph.inputs.items()}
|
|
219
|
+
if "vector" in input_names:
|
|
219
220
|
kwargs["vector"] = vec
|
|
220
221
|
|
|
221
222
|
return_type = result_graph.metadata.get("known_value_type", None)
|
|
@@ -238,6 +239,9 @@ def transpile_targets(
|
|
|
238
239
|
|
|
239
240
|
func_resolution, import_lines = default_func_resolution_map(graph)
|
|
240
241
|
for oprow in NODE_OPERATOR_TABLE:
|
|
242
|
+
# Python % is floored but Blender MODULO is truncated - keep the named call
|
|
243
|
+
if oprow.operator_type == cg.OperatorType.MOD:
|
|
244
|
+
continue
|
|
241
245
|
func_resolution[oprow.pf_func] = oprow.operator_type
|
|
242
246
|
|
|
243
247
|
python = to_python(
|
|
@@ -205,16 +205,29 @@ def is_zero_displacement(value: Any) -> bool:
|
|
|
205
205
|
Such a displacement has no effect, so the material build can leave the
|
|
206
206
|
output socket disconnected and skip emitting the subgraph entirely.
|
|
207
207
|
"""
|
|
208
|
-
from procfunc import compute_graph as cg
|
|
209
|
-
from procfunc.nodes import
|
|
208
|
+
from procfunc import compute_graph as cg # keep-local
|
|
209
|
+
from procfunc.nodes import math as node_math # keep-local
|
|
210
|
+
from procfunc.nodes import types as nt # keep-local
|
|
211
|
+
from procfunc.nodes.util.bindings_util import ContextualNode # keep-local
|
|
210
212
|
|
|
211
213
|
if isinstance(value, nt.ProcNode):
|
|
212
214
|
value = value.item()
|
|
213
|
-
if
|
|
215
|
+
if isinstance(value, cg.ConstantNode):
|
|
216
|
+
value = value.value
|
|
217
|
+
elif isinstance(value, cg.FunctionCallNode) and value.func is node_math.constant:
|
|
218
|
+
value = value.args[0]
|
|
219
|
+
elif (
|
|
220
|
+
isinstance(value, cg.ProceduralNode)
|
|
221
|
+
and value.node_type == ContextualNode.VECTOR.value
|
|
222
|
+
):
|
|
223
|
+
value = value.attrs.get("value")
|
|
224
|
+
else:
|
|
214
225
|
return False
|
|
215
|
-
return isinstance(value
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
return isinstance(value, (mathutils.Vector, tuple, list)) and tuple(value) == (
|
|
227
|
+
0.0,
|
|
228
|
+
0.0,
|
|
229
|
+
0.0,
|
|
230
|
+
)
|
|
218
231
|
|
|
219
232
|
|
|
220
233
|
@dataclass
|
|
@@ -226,7 +239,9 @@ class Material:
|
|
|
226
239
|
|
|
227
240
|
def item(self) -> bpy.types.Material:
|
|
228
241
|
if self._bpy_material is None:
|
|
229
|
-
from procfunc.nodes.execute.realize import
|
|
242
|
+
from procfunc.nodes.execute.realize import ( # keep-local
|
|
243
|
+
build_bpy_material,
|
|
244
|
+
)
|
|
230
245
|
|
|
231
246
|
self._bpy_material = build_bpy_material(
|
|
232
247
|
surface=self.surface,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import bpy
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
import procfunc as pf
|
|
5
|
+
from procfunc.codegen import codegen
|
|
6
|
+
from procfunc.codegen.identifiers import dedup_names_with_suffix
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _codegen_and_call(func, **inputs):
|
|
10
|
+
graph = pf.trace(func)
|
|
11
|
+
src = codegen.to_python(graph, toplevel_as_maincall=False)
|
|
12
|
+
namespace = {}
|
|
13
|
+
exec(src, namespace) # noqa: S102
|
|
14
|
+
return src, namespace[func.__name__](**inputs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_folded_operand_ending_in_call_keeps_parens():
|
|
18
|
+
def sub_of_add_astype(a, b, c):
|
|
19
|
+
return c - (a + b.astype(float))
|
|
20
|
+
|
|
21
|
+
src, result = _codegen_and_call(
|
|
22
|
+
sub_of_add_astype, a=np.float64(1.0), b=np.float64(2.0), c=np.float64(10.0)
|
|
23
|
+
)
|
|
24
|
+
assert "c - (a + b.astype(float))" in src, src
|
|
25
|
+
assert result == 7.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_negative_constant_pow_base_keeps_parens():
|
|
29
|
+
def pow_negative_base(x):
|
|
30
|
+
return (-2.0) ** x
|
|
31
|
+
|
|
32
|
+
src, result = _codegen_and_call(pow_negative_base, x=2.0)
|
|
33
|
+
assert "(-2.0) ** x" in src, src
|
|
34
|
+
assert result == 4.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _lamp_with_blackbody_emission():
|
|
38
|
+
light = pf.ops.primitives.light.point_lamp(100.0)
|
|
39
|
+
color = pf.nodes.color.blackbody(3000.0)
|
|
40
|
+
emit = pf.nodes.shader.emission(color=color, strength=5.0)
|
|
41
|
+
pf.nodes.execute.execute.to_light(light, surface=emit)
|
|
42
|
+
return light
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _tree_has_node(tree, bl_idname):
|
|
46
|
+
for n in tree.nodes:
|
|
47
|
+
if n.bl_idname == bl_idname:
|
|
48
|
+
return True
|
|
49
|
+
if n.bl_idname == "ShaderNodeGroup" and n.node_tree is not None:
|
|
50
|
+
if _tree_has_node(n.node_tree, bl_idname):
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_to_light_shader_survives_codegen():
|
|
56
|
+
graph = pf.trace(_lamp_with_blackbody_emission)
|
|
57
|
+
code = codegen.to_python(graph, toplevel_as_maincall=False)
|
|
58
|
+
assert "to_light" in code, code
|
|
59
|
+
|
|
60
|
+
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
61
|
+
namespace = {}
|
|
62
|
+
exec(code, namespace) # noqa: S102
|
|
63
|
+
namespace["_lamp_with_blackbody_emission"]()
|
|
64
|
+
|
|
65
|
+
lit = [
|
|
66
|
+
light
|
|
67
|
+
for light in bpy.data.lights
|
|
68
|
+
if light.use_nodes
|
|
69
|
+
and light.node_tree is not None
|
|
70
|
+
and _tree_has_node(light.node_tree, "ShaderNodeBlackbody")
|
|
71
|
+
]
|
|
72
|
+
assert lit, "re-executed light lost its Blackbody emission shader"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_dedup_suffix_collides_with_later_base_name():
|
|
76
|
+
names = {
|
|
77
|
+
0: "a_1", # strips to 'a'
|
|
78
|
+
1: "a_2", # strips to 'a'
|
|
79
|
+
2: "a_0_3", # strips to 'a_0'
|
|
80
|
+
}
|
|
81
|
+
result = dedup_names_with_suffix(
|
|
82
|
+
names,
|
|
83
|
+
separator="_",
|
|
84
|
+
order=[0, 1, 2],
|
|
85
|
+
first_use_suffix=True,
|
|
86
|
+
)
|
|
87
|
+
print(f"{result=}")
|
|
88
|
+
values = list(result.values())
|
|
89
|
+
assert len(values) == len(set(values)), f"Duplicate names in {values}"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import procfunc as pf
|
|
5
|
+
from procfunc import compute_graph as cg
|
|
6
|
+
|
|
7
|
+
_distribution_calls = {
|
|
8
|
+
pf.random.uniform: lambda rng: pf.random.uniform(rng, 0.1, 1.0),
|
|
9
|
+
pf.random.normal: lambda rng: pf.random.normal(rng, 0.0, 1.0),
|
|
10
|
+
pf.random.randint: lambda rng: pf.random.randint(rng, 0, 10),
|
|
11
|
+
pf.random.uniform_tails: lambda rng: pf.random.uniform_tails(rng, 0.0, 1.0),
|
|
12
|
+
pf.random.clip_gaussian: lambda rng: pf.random.clip_gaussian(rng, 0.0, 1.0),
|
|
13
|
+
pf.random.wrap_gaussian: lambda rng: pf.random.wrap_gaussian(
|
|
14
|
+
rng, 0.0, 1.0, -1.0, 1.0
|
|
15
|
+
),
|
|
16
|
+
pf.random.exponential: lambda rng: pf.random.exponential(rng, 1.0),
|
|
17
|
+
pf.random.log_uniform: lambda rng: pf.random.log_uniform(rng, 0.4, 2.0),
|
|
18
|
+
pf.random.log_normal: lambda rng: pf.random.log_normal(rng, 1.0, 0.5),
|
|
19
|
+
pf.random.spherical_sample: lambda rng: pf.random.spherical_sample(rng),
|
|
20
|
+
pf.random.mixture_of_gaussian: lambda rng: pf.random.mixture_of_gaussian(
|
|
21
|
+
rng, np.array([0.0, 5.0]), np.array([1.0, 1.0]), [1.0, 1.0]
|
|
22
|
+
),
|
|
23
|
+
pf.random.beta: lambda rng: pf.random.beta(rng, 2.0, 5.0),
|
|
24
|
+
pf.random.poisson: lambda rng: pf.random.poisson(rng, 3.0),
|
|
25
|
+
pf.random.triangular: lambda rng: pf.random.triangular(rng, 0.0, 1.0, 0.5),
|
|
26
|
+
pf.random.gamma: lambda rng: pf.random.gamma(rng, 2.0, 1.0),
|
|
27
|
+
pf.random.binomial: lambda rng: pf.random.binomial(rng, 10, 0.5),
|
|
28
|
+
pf.random.geometric: lambda rng: pf.random.geometric(rng, 0.3),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_distribution_call_table_is_complete():
|
|
33
|
+
assert set(_distribution_calls) >= set(pf.random.random_distrib_funcs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("func", list(_distribution_calls), ids=lambda f: f.__name__)
|
|
37
|
+
def test_distribution_bakes_to_constant_when_traced(func):
|
|
38
|
+
def generator(rng):
|
|
39
|
+
return _distribution_calls[func](rng)
|
|
40
|
+
|
|
41
|
+
graph = pf.trace(
|
|
42
|
+
generator,
|
|
43
|
+
rng=np.random.default_rng(0),
|
|
44
|
+
trace_level=pf.tracer.TraceLevel.GENERATORS,
|
|
45
|
+
)
|
|
46
|
+
out = list(graph.outputs.dict().values())[0]
|
|
47
|
+
assert isinstance(out, cg.ConstantNode)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_clip_gaussian_respects_zero_bounds():
|
|
51
|
+
"""low=0.0 / high=0.0 are real bounds, not "unset"."""
|
|
52
|
+
rng = np.random.default_rng(0)
|
|
53
|
+
samples = [
|
|
54
|
+
pf.random.clip_gaussian(
|
|
55
|
+
rng, mean=-0.5, std=1.0, low=0.0, high=2.0, max_tries=1000
|
|
56
|
+
)
|
|
57
|
+
for _ in range(50)
|
|
58
|
+
]
|
|
59
|
+
assert all(0.0 <= s <= 2.0 for s in samples)
|
|
60
|
+
|
|
61
|
+
samples = [
|
|
62
|
+
pf.random.clip_gaussian(
|
|
63
|
+
rng, mean=0.5, std=1.0, low=-2.0, high=0.0, max_tries=1000
|
|
64
|
+
)
|
|
65
|
+
for _ in range(50)
|
|
66
|
+
]
|
|
67
|
+
assert all(-2.0 <= s <= 0.0 for s in samples)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_clip_gaussian_default_bounds():
|
|
71
|
+
rng = np.random.default_rng(0)
|
|
72
|
+
samples = [pf.random.clip_gaussian(rng, mean=5.0, std=1.0) for _ in range(50)]
|
|
73
|
+
assert all(2.0 <= s <= 8.0 for s in samples)
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from procfunc.codegen.identifiers import dedup_names_with_suffix
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_dedup_suffix_collides_with_later_base_name():
|
|
5
|
-
names = {
|
|
6
|
-
0: "a_1", # strips to 'a'
|
|
7
|
-
1: "a_2", # strips to 'a'
|
|
8
|
-
2: "a_0_3", # strips to 'a_0'
|
|
9
|
-
}
|
|
10
|
-
result = dedup_names_with_suffix(
|
|
11
|
-
names,
|
|
12
|
-
separator="_",
|
|
13
|
-
order=[0, 1, 2],
|
|
14
|
-
first_use_suffix=True,
|
|
15
|
-
)
|
|
16
|
-
print(f"{result=}")
|
|
17
|
-
values = list(result.values())
|
|
18
|
-
assert len(values) == len(set(values)), f"Duplicate names in {values}"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
|
|
3
|
-
import procfunc as pf
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_clip_gaussian_respects_zero_bounds():
|
|
7
|
-
"""low=0.0 / high=0.0 are real bounds, not "unset"."""
|
|
8
|
-
rng = np.random.default_rng(0)
|
|
9
|
-
samples = [
|
|
10
|
-
pf.random.clip_gaussian(
|
|
11
|
-
rng, mean=-0.5, std=1.0, low=0.0, high=2.0, max_tries=1000
|
|
12
|
-
)
|
|
13
|
-
for _ in range(50)
|
|
14
|
-
]
|
|
15
|
-
assert all(0.0 <= s <= 2.0 for s in samples)
|
|
16
|
-
|
|
17
|
-
samples = [
|
|
18
|
-
pf.random.clip_gaussian(
|
|
19
|
-
rng, mean=0.5, std=1.0, low=-2.0, high=0.0, max_tries=1000
|
|
20
|
-
)
|
|
21
|
-
for _ in range(50)
|
|
22
|
-
]
|
|
23
|
-
assert all(-2.0 <= s <= 0.0 for s in samples)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_clip_gaussian_default_bounds():
|
|
27
|
-
rng = np.random.default_rng(0)
|
|
28
|
-
samples = [pf.random.clip_gaussian(rng, mean=5.0, std=1.0) for _ in range(50)]
|
|
29
|
-
assert all(2.0 <= s <= 8.0 for s in samples)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|