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.
Files changed (109) hide show
  1. {procfunc-0.34.0/src/procfunc.egg-info → procfunc-0.35.0}/PKG-INFO +1 -1
  2. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/__init__.py +1 -1
  3. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/codegen.py +77 -14
  4. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/repr.py +16 -15
  5. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/proxy.py +4 -0
  6. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/execute.py +1 -1
  7. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/realize.py +31 -12
  8. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/node_function.py +6 -0
  9. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/__init__.py +1 -0
  10. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/random.py +12 -0
  11. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/trace.py +16 -0
  12. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/bpy_to_computegraph.py +42 -5
  13. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/main.py +5 -1
  14. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/types.py +22 -7
  15. {procfunc-0.34.0 → procfunc-0.35.0/src/procfunc.egg-info}/PKG-INFO +1 -1
  16. procfunc-0.35.0/tests/test_codegen.py +89 -0
  17. procfunc-0.35.0/tests/test_random.py +73 -0
  18. procfunc-0.34.0/tests/test_codegen.py +0 -18
  19. procfunc-0.34.0/tests/test_random.py +0 -29
  20. {procfunc-0.34.0 → procfunc-0.35.0}/LICENSE.md +0 -0
  21. {procfunc-0.34.0 → procfunc-0.35.0}/README.md +0 -0
  22. {procfunc-0.34.0 → procfunc-0.35.0}/pyproject.toml +0 -0
  23. {procfunc-0.34.0 → procfunc-0.35.0}/setup.cfg +0 -0
  24. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/cli/__init__.py +0 -0
  25. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/cli/main.py +0 -0
  26. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/__init__.py +0 -0
  27. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/codegen/identifiers.py +0 -0
  28. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/color.py +0 -0
  29. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/__init__.py +0 -0
  30. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/compute_graph.py +0 -0
  31. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/node.py +0 -0
  32. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/operators_info.py +0 -0
  33. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/compute_graph/util.py +0 -0
  34. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/context.py +0 -0
  35. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/control/__init__.py +0 -0
  36. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/__init__.py +0 -0
  37. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/color.py +0 -0
  38. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/compositor.py +0 -0
  39. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_nodes.py +0 -0
  40. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_operator.py +0 -0
  41. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_special_cases.py +0 -0
  42. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/construct_standard.py +0 -0
  43. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/infer_runtime_data_type.py +0 -0
  44. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/execute/util.py +0 -0
  45. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/func.py +0 -0
  46. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/geo.py +0 -0
  47. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/manifest.json +0 -0
  48. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/math.py +0 -0
  49. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/shader.py +0 -0
  50. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/texture.py +0 -0
  51. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/types.py +0 -0
  52. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/__init__.py +0 -0
  53. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/bindings_util.py +0 -0
  54. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/nodes/util/bpy_node_info.py +0 -0
  55. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/__init__.py +0 -0
  56. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/_util.py +0 -0
  57. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/addons.py +0 -0
  58. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/attr.py +0 -0
  59. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/collection.py +0 -0
  60. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/curve.py +0 -0
  61. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/file.py +0 -0
  62. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/manifest.json +0 -0
  63. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/mesh.py +0 -0
  64. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/modifier.py +0 -0
  65. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/object.py +0 -0
  66. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/camera.py +0 -0
  67. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/curve.py +0 -0
  68. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/light.py +0 -0
  69. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/primitives/mesh.py +0 -0
  70. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/ops/uv.py +0 -0
  71. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/__init__.py +0 -0
  72. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/decorator.py +0 -0
  73. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/patch.py +0 -0
  74. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/tracer/proxy.py +0 -0
  75. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/__init__.py +0 -0
  76. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/cleanup.py +0 -0
  77. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/convert.py +0 -0
  78. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/distribution.py +0 -0
  79. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/extract_materials.py +0 -0
  80. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/infer_distribution.py +0 -0
  81. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/parameters.py +0 -0
  82. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transforms/util.py +0 -0
  83. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/__init__.py +0 -0
  84. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/parse_default_values.py +0 -0
  85. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/transpiler/parse_special_cases.py +0 -0
  86. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/__init__.py +0 -0
  87. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/bpy_data.py +0 -0
  88. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/bpy_info.py +0 -0
  89. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/camera.py +0 -0
  90. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/keyframe.py +0 -0
  91. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/log.py +0 -0
  92. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/manifest.py +0 -0
  93. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/pytree.py +0 -0
  94. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc/util/teardown.py +0 -0
  95. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/SOURCES.txt +0 -0
  96. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/dependency_links.txt +0 -0
  97. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/entry_points.txt +0 -0
  98. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/requires.txt +0 -0
  99. {procfunc-0.34.0 → procfunc-0.35.0}/src/procfunc.egg-info/top_level.txt +0 -0
  100. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_asset.py +0 -0
  101. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_bpy_data_cleanup.py +0 -0
  102. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_cli_transpile.py +0 -0
  103. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_codegen_matrix.py +0 -0
  104. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_compute_graph.py +0 -0
  105. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_node_function.py +0 -0
  106. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_ops.py +0 -0
  107. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_pytree.py +0 -0
  108. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_trace.py +0 -0
  109. {procfunc-0.34.0 → procfunc-0.35.0}/tests/test_transforms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procfunc
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Function-Oriented Abstractions for Procedural 3D Generation in Python
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: Homepage, https://github.com/princeton-vl/procfunc
@@ -2,7 +2,7 @@
2
2
  # ensure this gets imported first so that mathutils etc is available even if later modules dont import bpy
3
3
  import bpy
4
4
 
5
- __version__ = "0.34.0"
5
+ __version__ = "0.35.0"
6
6
 
7
7
  from numpy.random import Generator as RNG
8
8
 
@@ -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 " " in expr and extra_parens and expr[0] != "(" and expr[-1] != ")":
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
- func_str = f"{_repr_inp(target, scope_expressions)}.{method_name}"
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 [bound.arguments[name] for name in operand_names]
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: x.kwargs.get("default_value", None) is not None,
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 (default := node.kwargs.get("default_value")) is not None:
309
- line += f" = {repr_value(default)}"
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
- vt = node.metadata.get("known_value_type", None)
326
- if vt is None:
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
- logger.warning(
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
- logger.warning(
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
 
@@ -67,7 +67,7 @@ def to_environment(
67
67
  return pt.World(world)
68
68
 
69
69
 
70
- @tracer_primitive
70
+ @tracer_primitive(mutates=["light"])
71
71
  def to_light(
72
72
  light: pt.LightObject,
73
73
  surface: nt.ProcNode[nt.Shader],
@@ -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 NodeGroupType, SocketType
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 .construct_nodes import as_nodegroup, instantiate_nodegroup
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 = pytree.PyTree(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}),
@@ -20,6 +20,7 @@ from .mesh import (
20
20
  mesh_cone,
21
21
  mesh_cube,
22
22
  mesh_cylinder,
23
+ mesh_from_numpy,
23
24
  mesh_grid,
24
25
  mesh_icosphere,
25
26
  mesh_line,
@@ -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
- outsockets = get_active_sockets(link.from_node.outputs)
236
- assert len(outsockets) > 0
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
- if default_value is not None:
916
- node.kwargs["default_value"] = default_value
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
- if result_graph.name.startswith("material_"):
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 types as nt
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 not isinstance(value, cg.ConstantNode):
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.value, (mathutils.Vector, tuple, list)) and tuple(
216
- value.value
217
- ) == (0.0, 0.0, 0.0)
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 build_bpy_material
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procfunc
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Function-Oriented Abstractions for Procedural 3D Generation in Python
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: Homepage, https://github.com/princeton-vl/procfunc
@@ -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