deformable 0.1.0__py3-none-win_amd64.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.
@@ -0,0 +1,468 @@
1
+ """The simulation a host drives frame by frame, with its value types.
2
+
3
+ Lengths cross in host units; time, forces, gravity, masses and material values in simulation units. The simulation's
4
+ scale converts host lengths to simulation lengths.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ import dataclasses
11
+ import numbers
12
+ from collections.abc import Callable, Sequence
13
+ from typing import Any, NamedTuple
14
+
15
+ from . import conversion, library, native
16
+ from .cancel_flag import CancelFlag, cancel_handle
17
+ from .errors import Status, check
18
+ from .handles import OwnedHandle
19
+ from .meshes import tetrahedral_mesh_view
20
+ from .values import StepOutcome, Vector3
21
+
22
+ _UNIFORM = 0
23
+ _PER_TETRAHEDRON = 1
24
+
25
+
26
+ @dataclasses.dataclass(frozen=True)
27
+ class MaterialParameters:
28
+ """Material values as a host gives them, before the library clamps them."""
29
+
30
+ youngs_modulus: float
31
+ """Young's modulus E, in pascals in simulation units."""
32
+ poissons_ratio: float
33
+ """Poisson's ratio ν, unitless."""
34
+ density: float
35
+ """Mass per unit volume; a tetrahedron weighs density·V."""
36
+ toughness: float
37
+ """Fracture threshold on half the largest separation-tensor eigenvalue; 0 or negative means unbreakable."""
38
+ youngs_damping: float
39
+ """Stiffness-proportional damping; the engine uses alpha = youngs_damping / youngs_modulus."""
40
+ yield_strain: float
41
+ """Plastic flow starts when the elastic strain norm exceeds this."""
42
+ maximum_yield: float
43
+ """Bound on the plastic strain norm; 0 resets the plastic strain every solve."""
44
+ creep_fraction: float
45
+ """Fraction, 0 to 1, of the elastic strain added to the plastic strain once per solve, not per second, while its
46
+ norm exceeds yield_strain; 0 disables plasticity."""
47
+ wake_up_radius: float
48
+ """Radius of the impact wake flood fill, in simulation units."""
49
+ friction: float
50
+ """Coulomb friction coefficient."""
51
+
52
+ @classmethod
53
+ def default(cls, **changes: float) -> MaterialParameters:
54
+ """The values a new material starts from, with the given fields changed."""
55
+ structure = native.MaterialParameters()
56
+ check(library.load().deformable_material_parameters_default(ctypes.byref(structure)))
57
+ return dataclasses.replace(cls._from_native(structure), **changes)
58
+
59
+ @classmethod
60
+ def presets(cls) -> list[MaterialPreset]:
61
+ """The presets the library ships, in its order: from the softest to the metals."""
62
+ functions = library.load()
63
+ return [cls._preset_at(functions, index) for index in range(functions.deformable_material_preset_count())]
64
+
65
+ @classmethod
66
+ def preset(cls, name: str) -> MaterialParameters | None:
67
+ """The preset with this name, or None. The name must match exactly, case included ("unbreakable_rubber")."""
68
+ if not isinstance(name, str):
69
+ raise TypeError(f"a preset name is a str, not {type(name).__name__}")
70
+ if "\0" in name:
71
+ return None
72
+ # Lone surrogates pass through as bytes that are not UTF-8, which no preset name matches.
73
+ encoded = ctypes.create_string_buffer(name.encode("utf-8", errors="surrogatepass"))
74
+ structure = native.MaterialParameters()
75
+ code = library.load().deformable_material_preset_find(encoded, ctypes.byref(structure))
76
+ if code == Status.INVALID_ARGUMENT:
77
+ return None
78
+ check(code)
79
+ return cls._from_native(structure)
80
+
81
+ @classmethod
82
+ def _preset_at(cls, functions: ctypes.CDLL, index: int) -> MaterialPreset:
83
+ name = ctypes.POINTER(ctypes.c_char)()
84
+ structure = native.MaterialParameters()
85
+ check(functions.deformable_material_preset(index, ctypes.byref(name), ctypes.byref(structure)))
86
+ return MaterialPreset(ctypes.string_at(name).decode("utf-8"), cls._from_native(structure))
87
+
88
+ @classmethod
89
+ def _from_native(cls, structure: native.MaterialParameters) -> MaterialParameters:
90
+ return cls(**{field.name: getattr(structure, field.name) for field in dataclasses.fields(cls)})
91
+
92
+ def _native(self) -> native.MaterialParameters:
93
+ return native.MaterialParameters(**dataclasses.asdict(self))
94
+
95
+
96
+ class MaterialPreset(NamedTuple):
97
+ """A material preset the library ships: its name and its values."""
98
+
99
+ name: str
100
+ """The preset's lowercase snake_case name, such as "concrete" or "unbreakable_rubber"."""
101
+ parameters: MaterialParameters
102
+ """The values, each inside its clamp range, so add_material keeps them as they are."""
103
+
104
+
105
+ @dataclasses.dataclass(frozen=True)
106
+ class ObjectOptions:
107
+ """The per-object settings."""
108
+
109
+ self_collision: bool
110
+ """The object's colliders test its own faces against each other."""
111
+ disabled: bool
112
+ """The object is created disabled."""
113
+
114
+ @classmethod
115
+ def default(cls, **changes: bool) -> ObjectOptions:
116
+ """The object defaults (self-collision on, enabled), with the given fields changed."""
117
+ structure = native.ObjectOptions()
118
+ check(library.load().deformable_object_options_default(ctypes.byref(structure)))
119
+ values = {"self_collision": bool(structure.self_collision), "disabled": bool(structure.disabled)}
120
+ return cls(**{**values, **changes})
121
+
122
+ def _native(self) -> native.ObjectOptions:
123
+ return native.ObjectOptions(bool(self.self_collision), bool(self.disabled))
124
+
125
+
126
+ class GlueParameters(NamedTuple):
127
+ """The spring and damping of glue; a zero spring constant or damping coefficient takes 4905 or 2452.5, each on
128
+ its own."""
129
+
130
+ spring_constant: float
131
+ """The spring constant k."""
132
+ damping_coefficient: float
133
+ """The damping coefficient c."""
134
+
135
+
136
+ class Plane(NamedTuple):
137
+ """A bounding plane of a convex region: the inside is normal · p + offset <= 0."""
138
+
139
+ normal: Sequence[float]
140
+ """The outward normal n, of any length."""
141
+ offset: float
142
+ """The offset d."""
143
+
144
+
145
+ @dataclasses.dataclass(frozen=True)
146
+ class ConvexRegion:
147
+ """A convex region: the points inside all of its planes; with no plane it holds every point."""
148
+
149
+ planes: Sequence[Plane]
150
+ """The bounding planes, each a Plane or a (normal, offset) pair."""
151
+
152
+ def contains(self, point: Sequence[float]) -> bool:
153
+ """True when normal · point + offset <= 0 for every plane: the test a host runs for passive regions."""
154
+ inside = native.Boolean()
155
+ check(
156
+ library.load().deformable_convex_region_contains(
157
+ ctypes.byref(self._native()), conversion.vector3(point), ctypes.byref(inside)
158
+ )
159
+ )
160
+ return bool(inside.value)
161
+
162
+ def _native(self) -> native.ConvexRegion:
163
+ planes = conversion.structure_array(
164
+ self.planes, native.Plane, lambda plane: native.Plane(conversion.vector3(plane[0]), plane[1])
165
+ )
166
+ return native.ConvexRegion(planes, len(planes))
167
+
168
+
169
+ class DrivenVertex(NamedTuple):
170
+ """A vertex of an object driven to a target, in host units."""
171
+
172
+ vertex: int
173
+ """The vertex, an index into the object's nodes."""
174
+ target: Sequence[float]
175
+ """Where the vertex is at the end of the next step, in host units."""
176
+
177
+
178
+ class StepReport(NamedTuple):
179
+ """The result of a Simulation.step call."""
180
+
181
+ substeps_run: int
182
+ """The number of substeps simulated."""
183
+ outcome: StepOutcome
184
+ """What the call did."""
185
+
186
+
187
+ @dataclasses.dataclass(frozen=True)
188
+ class TetrahedronState:
189
+ """The output record of one tetrahedron."""
190
+
191
+ corners: tuple[Vector3, Vector3, Vector3, Vector3]
192
+ """The current corner positions in host units."""
193
+ island: int
194
+ """The id of the tetrahedron's island, or 0 while it has none."""
195
+ strain_measure: float
196
+ """The squared norm of the total co-rotated strain, plastic part included."""
197
+ flags: int
198
+ """The exposed-face bits 0-3 reported so far (sticky), plus NODE_REPLACED."""
199
+
200
+ def face_centroid(self, face: int) -> Vector3:
201
+ """The centroid of face face (0-3, the face opposite corner face)."""
202
+ return self._face_query("deformable_tetrahedron_state_face_centroid", face)
203
+
204
+ def face_normal(self, face: int) -> Vector3:
205
+ """The outward unit normal of face face (0-3) of a positively oriented (left-handed) state; zero for a face of
206
+ zero area."""
207
+ return self._face_query("deformable_tetrahedron_state_face_normal", face)
208
+
209
+ def _face_query(self, function_name: str, face: int) -> Vector3:
210
+ result = native.Vector3()
211
+ function = getattr(library.load(), function_name)
212
+ check(function(ctypes.byref(self._native()), conversion.unsigned(face), ctypes.byref(result)))
213
+ return conversion.from_vector3(result)
214
+
215
+ def _native(self) -> native.TetrahedronState:
216
+ corners = (native.Vector3 * 4)(*(conversion.vector3(corner) for corner in self.corners))
217
+ return native.TetrahedronState(corners, self.island, self.strain_measure, self.flags)
218
+
219
+ @classmethod
220
+ def _from_native(cls, structure: native.TetrahedronState) -> TetrahedronState:
221
+ corners = tuple(conversion.from_vector3(corner) for corner in structure.corners)
222
+ return cls(corners, structure.island, structure.strain_measure, structure.flags)
223
+
224
+
225
+ class FaceEvent(NamedTuple):
226
+ """A tetrahedron fracture changed in the last step and the faces it now shares with no other tetrahedron."""
227
+
228
+ tetrahedron: int
229
+ """The tetrahedron's index in its object's mesh."""
230
+ exposed_faces: int
231
+ """Bit k is set when face k, opposite corner k, is exposed; original boundary faces count as exposed."""
232
+
233
+
234
+ class SimulationStatistics(NamedTuple):
235
+ """The solver, contact and island counters."""
236
+
237
+ systems: int
238
+ """Systems solved so far."""
239
+ conjugate_gradient_updates: int
240
+ """Conjugate gradient solution updates summed over every solve so far."""
241
+ contacts: int
242
+ """Live contacts, glue contacts included."""
243
+ islands: int
244
+ """Islands, awake and asleep."""
245
+
246
+
247
+ def tetrahedron_materials(materials: int | Sequence[int] | Any) -> native.TetrahedronMaterials:
248
+ """One material handle for every tetrahedron, or one handle per tetrahedron in mesh order."""
249
+ if isinstance(materials, numbers.Integral):
250
+ return native.TetrahedronMaterials(_UNIFORM, conversion.unsigned(materials), None, 0)
251
+ per_tetrahedron = conversion.index_array(materials, "materials")
252
+ return native.TetrahedronMaterials(_PER_TETRAHEDRON, 0, per_tetrahedron, len(per_tetrahedron))
253
+
254
+
255
+ class Simulation(OwnedHandle):
256
+ """One scene: gravity (0, -9.81, 0), scale 1 and min(available parallelism, 16) workers when created.
257
+
258
+ Use it in a with block, or call close(), to free it at a known point. It may move between threads; calls on one
259
+ simulation from several threads run one at a time, and close() waits for the call in flight. Object and material
260
+ handles are ints counting up from 0 in creation order.
261
+ """
262
+
263
+ _destroy_name = "deformable_simulation_destroy"
264
+ _description = "simulation"
265
+ _one_call_at_a_time = True
266
+
267
+ def __init__(self) -> None:
268
+ super().__init__("deformable_simulation_create")
269
+
270
+ def set_scale(self, scale: float) -> None:
271
+ """Sets the host length unit → simulation unit factor, in [0.001, 1000]; it applies to later conversions."""
272
+ self._call("deformable_simulation_set_scale", scale)
273
+
274
+ def set_gravity(self, gravity: Sequence[float]) -> None:
275
+ """Sets the gravity in simulation units, not scaled."""
276
+ self._call("deformable_simulation_set_gravity", conversion.vector3(gravity))
277
+
278
+ def set_worker_count(self, count: int) -> None:
279
+ """Sets the worker count, at least 1 and including the calling thread; results do not depend on it."""
280
+ self._call("deformable_simulation_set_worker_count", conversion.size(count))
281
+
282
+ def add_material(self, parameters: MaterialParameters) -> int:
283
+ """Creates a material, each value clamped to its range, and returns its handle."""
284
+ material = native.Index()
285
+ self._call("deformable_simulation_add_material", ctypes.byref(parameters._native()), ctypes.byref(material))
286
+ return material.value
287
+
288
+ def set_material(self, material: int, parameters: MaterialParameters) -> None:
289
+ """Rewrites a material's values; the objects using it take them before the next step's substeps."""
290
+ self._call(
291
+ "deformable_simulation_set_material", conversion.unsigned(material), ctypes.byref(parameters._native())
292
+ )
293
+
294
+ def add_object(self, mesh: Any, materials: int | Sequence[int] | Any, options: ObjectOptions | None = None) -> int:
295
+ """Creates an object from a tetrahedral mesh in world space and host units, and returns its handle.
296
+
297
+ materials is one material handle for every tetrahedron or one per tetrahedron; options defaults to
298
+ ObjectOptions.default(). A degenerate tetrahedron or a face shared by three tetrahedrons raises a
299
+ DeformableError naming it, and nothing is created then.
300
+ """
301
+ options = ObjectOptions.default() if options is None else options
302
+ object_handle = native.Index()
303
+ self._call(
304
+ "deformable_simulation_add_object",
305
+ ctypes.byref(tetrahedral_mesh_view(mesh)),
306
+ ctypes.byref(tetrahedron_materials(materials)),
307
+ ctypes.byref(options._native()),
308
+ ctypes.byref(object_handle),
309
+ )
310
+ return object_handle.value
311
+
312
+ def set_object_material(self, object_handle: int, materials: int | Sequence[int] | Any) -> None:
313
+ """Reassigns an object's materials before the next step's substeps; it rebuilds islands and wakes nothing."""
314
+ self._call(
315
+ "deformable_simulation_set_object_material",
316
+ conversion.unsigned(object_handle),
317
+ ctypes.byref(tetrahedron_materials(materials)),
318
+ )
319
+
320
+ def remove_object(self, object_handle: int) -> None:
321
+ """Deletes an object; its contacts and glue are freed and its handle becomes unknown."""
322
+ self._call("deformable_simulation_remove_object", conversion.unsigned(object_handle))
323
+
324
+ def set_object_disabled(self, object_handle: int, disabled: bool) -> None:
325
+ """Disables or enables an object; a disabled object keeps its outputs and ignores forces and driving."""
326
+ self._call("deformable_simulation_set_object_disabled", conversion.unsigned(object_handle), bool(disabled))
327
+
328
+ def set_self_collision(self, object_handle: int, enabled: bool) -> None:
329
+ """Sets or clears an object's self-collision; an island takes the change at its next rebuild."""
330
+ self._call("deformable_simulation_set_self_collision", conversion.unsigned(object_handle), bool(enabled))
331
+
332
+ def set_never_sleep(self, object_handle: int, node: int, enabled: bool) -> None:
333
+ """Sets or clears the never-sleep flag of a node: its island never sleeps, and setting it wakes the node."""
334
+ self._call(
335
+ "deformable_simulation_set_never_sleep",
336
+ conversion.unsigned(object_handle),
337
+ conversion.unsigned(node),
338
+ bool(enabled),
339
+ )
340
+
341
+ def set_glue_all(self, parameters: GlueParameters | None) -> None:
342
+ """Glues each new object to every other enabled object where they overlap; None turns glue all off."""
343
+ glue = None if parameters is None else ctypes.byref(native.GlueParameters(*parameters))
344
+ self._call("deformable_simulation_set_glue_all", glue)
345
+
346
+ def add_glue_region(self, region: ConvexRegion, members: Sequence[int], parameters: GlueParameters) -> None:
347
+ """Glues each new member to every other enabled member through their tetrahedrons with a vertex inside every
348
+ plane of region; plane offsets are in host units."""
349
+ member_handles = conversion.index_array(members, "members")
350
+ self._call(
351
+ "deformable_simulation_add_glue_region",
352
+ ctypes.byref(region._native()),
353
+ member_handles,
354
+ len(member_handles),
355
+ ctypes.byref(native.GlueParameters(*parameters)),
356
+ )
357
+
358
+ def drive(self, object_handle: int, vertices: Sequence[DrivenVertex]) -> None:
359
+ """Drives the listed vertices toward their targets (host units) from the next step on, until the next call
360
+ for the object; previously driven vertices not listed are released with their last driven velocity."""
361
+ driven = conversion.structure_array(
362
+ vertices,
363
+ native.DrivenVertex,
364
+ lambda vertex: native.DrivenVertex(conversion.unsigned(vertex[0]), conversion.vector3(vertex[1])),
365
+ )
366
+ self._call("deformable_simulation_drive", conversion.unsigned(object_handle), driven, len(driven))
367
+
368
+ def add_force(self, object_handle: int, node: int, force: Sequence[float]) -> None:
369
+ """Queues a force in simulation units on a node for every substep of the next step; calls add up."""
370
+ self._call(
371
+ "deformable_simulation_add_force",
372
+ conversion.unsigned(object_handle),
373
+ conversion.unsigned(node),
374
+ conversion.vector3(force),
375
+ )
376
+
377
+ def step(self, time: float, substeps: int, cancel: CancelFlag | None = None) -> StepReport:
378
+ """Advances to time (seconds) in substeps equal substeps. The first call only records the time.
379
+
380
+ A cancel flag set from another thread stops the step between two substeps, after at least one.
381
+ """
382
+ report = native.StepReport()
383
+ with cancel_handle(cancel) as cancel_address:
384
+ self._call(
385
+ "deformable_simulation_step", time, conversion.unsigned(substeps), cancel_address, ctypes.byref(report)
386
+ )
387
+ return StepReport(report.substeps_run, StepOutcome(report.outcome))
388
+
389
+ def skip_to(self, time: float) -> None:
390
+ """Scene disable: records time as the last time without stepping, and drops the queued forces."""
391
+ self._call("deformable_simulation_skip_to", time)
392
+
393
+ def node_positions(self, object_handle: int) -> list[Vector3]:
394
+ """The current node positions of an object in host units; fracture adds nodes."""
395
+ return self._read_lent(
396
+ "deformable_simulation_node_positions", object_handle, native.Vector3, conversion.read_vector3_list
397
+ )
398
+
399
+ def node_velocities(self, object_handle: int) -> list[Vector3]:
400
+ """The current node velocities of an object in host units per second, in node-position order."""
401
+ return self._read_lent(
402
+ "deformable_simulation_node_velocities", object_handle, native.Vector3, conversion.read_vector3_list
403
+ )
404
+
405
+ def node_masses(self, object_handle: int) -> list[float]:
406
+ """The lumped node masses of an object in simulation units."""
407
+ return self._read_lent(
408
+ "deformable_simulation_node_masses",
409
+ object_handle,
410
+ ctypes.c_double,
411
+ lambda pointer, count: conversion.read_scalars(pointer, ctypes.c_double, count),
412
+ )
413
+
414
+ def tetrahedron_nodes(self, object_handle: int) -> list[tuple[int, int, int, int]]:
415
+ """The current four nodes of each tetrahedron of an object, in mesh order."""
416
+ return self._read_lent(
417
+ "deformable_simulation_tetrahedron_nodes",
418
+ object_handle,
419
+ native.TetrahedronIndices,
420
+ lambda pointer, count: conversion.read_groups(pointer, ctypes.c_uint32, count, 4),
421
+ )
422
+
423
+ def tetrahedron_states(self, object_handle: int) -> list[TetrahedronState]:
424
+ """The output record of each tetrahedron of an object, in mesh order."""
425
+ return self._read_lent(
426
+ "deformable_simulation_tetrahedron_states",
427
+ object_handle,
428
+ native.TetrahedronState,
429
+ lambda pointer, count: [
430
+ TetrahedronState._from_native(state)
431
+ for state in conversion.lent_array(pointer, native.TetrahedronState, count)
432
+ ],
433
+ )
434
+
435
+ def face_events(self, object_handle: int) -> list[FaceEvent]:
436
+ """The exposed-face events of an object from the last step, one per tetrahedron fracture changed."""
437
+ return self._read_lent(
438
+ "deformable_simulation_face_events",
439
+ object_handle,
440
+ native.FaceEvent,
441
+ lambda pointer, count: [
442
+ FaceEvent(event.tetrahedron, event.exposed_faces)
443
+ for event in conversion.lent_array(pointer, native.FaceEvent, count)
444
+ ],
445
+ )
446
+
447
+ def awake_tetrahedron_count(self) -> int:
448
+ """The number of tetrahedrons in awake islands."""
449
+ count = native.Count()
450
+ self._call("deformable_simulation_awake_tetrahedron_count", ctypes.byref(count))
451
+ return count.value
452
+
453
+ def statistics(self) -> SimulationStatistics:
454
+ """The solver, contact and island counters."""
455
+ statistics = native.SimulationStatistics()
456
+ self._call("deformable_simulation_statistics", ctypes.byref(statistics))
457
+ return SimulationStatistics(
458
+ statistics.systems, statistics.conjugate_gradient_updates, statistics.contacts, statistics.islands
459
+ )
460
+
461
+ def _read_lent(self, function_name: str, object_handle: int, element_type, read: Callable[[Any, int], Any]) -> Any:
462
+ """Copies an array the simulation lends with read(pointer, count) before another call can change it."""
463
+ pointer = ctypes.POINTER(element_type)()
464
+ count = native.Count()
465
+ with self._use() as address:
466
+ function = getattr(library.load(), function_name)
467
+ check(function(address, conversion.unsigned(object_handle), ctypes.byref(pointer), ctypes.byref(count)))
468
+ return read(pointer, count.value)