Y YASPS
Docs menu
Getting started Mental model Attributes JOIN UNION Optimization Dynamic contact Capstone example Examples API reference

Core syntax · chapter 03

Attributes and expressions

attribute is the symbolic value type used everywhere in YASPS. It describes one rows × cols value per instance of its correspondence. The same class represents stored data, constants, computed expressions, JOIN results, UNION results, and requested outputs.

Choose the right construction

Need Construction Differentiable? Storage
Degrees of freedom or mutable input owner.addAttribute(name, rows, cols) Yes Uploaded GPU data
Mutable history/material/control value owner.addConstant(name, rows, cols) No Uploaded GPU data
Reusable symbolic expression owner.addAttribute(name, computed_attribute=expr) Follows children Fused unless materialized
Gather through topology primitive.addAttribute(name, through=..., source=...) Follows source path Symbolic JOIN
Stack heterogeneous populations union.addAttribute(name) Follows child paths Symbolic UNION
Numeric literal or matrix attribute(float_value=...), to_array(...) No independent storage Inlined into expression

Data attributes

Create data through the scene, mesh, primitive, or union that owns its instance index:

position = vertices.addAttribute(
  "position",
  rows=3,
  cols=1,
)

Parameters

owner.addAttribute(
  name,
  computed_attribute=None,
  rows=1,
  cols=1,
)
Parameter Meaning
name Unique name in owner.attributes; used in generated identifiers
computed_attribute Leave None for a stored DATA leaf
rows Rows in each instance value
cols Columns in each instance value

The expected flattened value count is owner.numInstances * rows * cols.

Uploading values

position.updateValue(initial_positions)

updateValue(value, deepCopy=False) accepts:

Input Behavior
NumPy array or array-like value Flattens, converts to float64, allocates/uploads a PyCUDA array
PyCUDA GPUArray, deepCopy=False Makes the attribute reference that device array
PyCUDA GPUArray, deepCopy=True Reuses or allocates owned storage and copies values

Use deepCopy=True when the input is a temporary expression or aliases a reusable solver buffer:

position.updateValue(
  position.value - alpha * direction,
  deepCopy=True,
)

The implementation does not currently reject an incorrectly sized upload at this boundary. Supply exactly the flattened size expected by the correspondence and per-instance shape.

Constant attributes

mass = vertices.addConstant(
  "mass",
  rows=1,
  cols=1,
)
mass.updateValue(vertex_masses)

Parameters

owner.addConstant(name, rows=1, cols=1)
Parameter Meaning
name Unique attribute name on the owner
rows, cols Per-instance shape

CONSTANT means “derivative is zero,” not “immutable.” Previous positions, velocities, masses, rest geometry, material parameters, timestep, barrier distance, and runtime controls are all commonly constants even though the application updates them.

Computed attributes

Arithmetic creates unnamed expression nodes:

displacement = position - rest_position
quadratic_expression = 0.5 * stiffness * displacement.dot(displacement)

Bind a reusable node to its correspondence:

quadratic = vertices.addAttribute(
  "quadratic",
  computed_attribute=quadratic_expression,
)

Why name expressions

Named nodes provide:

  • stable generated identifiers;
  • reusable symbolic boundaries;
  • a place to request compute();
  • the required named scalar passed to addEnergy;
  • natural JIT kernel boundaries when an expression is explicitly materialized.

On a primitive, if a computed constant matrix has no correspondence, addAttribute assigns the primitive. If an expression belongs to a different correspondence, the primitive path copies the symbolic wrapper and retargets its correspondence; ordinary modeling should still respect lineage rather than relying on that fallback.

Primitive addAttribute modes

The full primitive signature is:

primitive.addAttribute(
  name,
  computed_attribute=None,
  rows=1,
  cols=1,
  through=None,
  source=None,
  operation=None,
)
Parameter Used when Meaning
name Always New unique name on the primitive
computed_attribute Computed mode Expression to bind; this mode takes priority
rows, cols Data mode Per-instance shape for a DATA leaf
through JOIN mode Outgoing connectivity whose fromPrimitive is this primitive
source Fixed JOIN Explicit attribute on the connectivity target
operation Variable JOIN "SUM" or "AVERAGE" when through.dimension == 0

The selection order is computed expression, then JOIN, then new DATA leaf. Do not provide arguments from multiple modes.

JOIN attributes

Fixed-arity JOIN gathers a target attribute:

tet_position = tets.addAttribute(
  "position",
  through=tet2vertex,
  source=vertices["position"],
)

If the connectivity dimension is k and source shape is r × c, the result shape is k × (r*c).

When source is omitted, YASPS looks up an attribute with the new name on the target primitive:

tet_position = tets.addAttribute(
  "position",
  through=tet2vertex,
)

For variable-arity connectivity (dimension=0), provide operation="SUM" or "AVERAGE" and use implicit same-name lookup. See Connectivity and JOIN.

UNION attributes

Create a union on a mesh, then query a same-named child attribute:

all_vertices = collision.addPrimitiveUnion(
  "vertices",
  [soft_vertices, affine_vertices],
)
all_position = all_vertices.addAttribute("position")

Every child must expose position with identical shape. The result retains the route to each child rather than becoming an eager concatenation.

The full union signature is:

union.addAttribute(
  name,
  computed_attribute=None,
  rows=0,
  cols=0,
)
Form Meaning
addAttribute(name) UNION same-named child attributes
addAttribute(name, computed_attribute=expr) Bind a union-owned expression
addAttribute(name, rows=r, cols=c) Create union-owned DATA rather than stack children

See Primitive unions for ordering, nested unions, and sparse symbolic matrices.

Literal and matrix construction

from yasps import attribute

scalar = attribute(float_value=2.0)
zero = attribute.zeros(3, 3)
identity = attribute.identity(3)

gravity = attribute.to_array(
  [0.0, -9.8, 0.0],
  rows=3,
  cols=1,
)

attribute.to_array(children, rows, cols) uses row-major order. rows * cols must equal len(children). Python floats and ints become literal nodes; other elements must be attributes with compatible lineage.

Shape, indexing, and views

scalar = A[1, 2]
flat_scalar = A[5]
row = A.row(1)
column = A.col(2)
transposed = A.transpose()
reshaped = joined.resize(3, 3)
Member Constraint Result
a[i] i < rows*cols Flattened scalar access
a[row, col] Indices in bounds Matrix scalar access
row(index) index < rows 1 × cols expression
col(index) index < cols rows × 1 expression
transpose() Any shape Shape becomes cols × rows
resize(rows, cols) Same element count New symbolic reshape
reshape(rows, cols) Same element count Mutates this Python object’s metadata

Prefer resize inside expressions. reshape changes the object in place and returns None.

Arithmetic

Syntax Shape rule Meaning
a + b, a - b Equal shapes or one scalar Add/subtract; scalar broadcasts
a * b a.cols == b.rows, or one scalar Matrix product or scalar multiplication
a / b b scalar Division
a.pow(p) Both scalar Power
-a Any shape Negation

Element-expanded variants are add_explicit, sub_explicit, mul_explicit, and div_explicit. They construct scalar components directly and are useful for derivative simplification or generated-code control.

The ordinary operators simplify zero, identity, and literal cases before creating a symbolic node.

Scalar functions

root = x.sqrt()
log_x = x.log()
sin_x = x.sin()
cos_x = x.cos()
angle = y.atan2(x)
magnitude = x.abs()
power = x.pow(2.0)

sqrt, log, sin, cos, atan2, and pow require scalar inputs. Literal inputs are folded in Python. log(0) and division by a symbolic zero are rejected.

Vector and matrix functions

dot = u.dot(v)
cross = u.cross(v)
length = u.norm()
trace = A.trace()
determinant = A.determinant()
inverse = A.inverse()
Member Constraint
dot(other) Both row/column vectors with equal flattened size
cross(other) Exactly three elements each
norm() Row or column vector
trace() Square matrix
determinant() Square matrix
inverse() Square matrix; scalar inverse becomes 1/a

dot and cross currently expand to scalar operations.

Conditions and selection

condition = distance >= threshold
selected = attribute.select(
  condition,
  active_energy,
  zero_energy,
)

eq, neq, >, and >= produce scalar condition attributes. The true and false branches passed to select must share shape and compatible lineage.

Projection and derivative control

spd

projected = A.spd(spd_method=2)
Value Eigenvalue policy
0 No numerical change
1 Absolute value
2 Clamp negative values to zero

A must be square. An integer method is stored in a generated scene constant; an attribute method may be used when it has compatible heritage.

asConstant

frozen_normal = normal.asConstant()

This keeps numerical evaluation in the generated expression but prevents differentiation through the wrapped value.

Materialization and execution

Expression construction does not launch work:

energy_expression = 0.5 * displacement.dot(displacement)

Request a full per-instance output with:

energy_gpu = energy_expression.compute().value
energy_cpu = energy_gpu.get()

On first compute, YASPS generates a device expression, creates a global kernel, allocates an output buffer, and JIT-compiles. Later calls reuse the generated kernel and enlarge storage only when required. Computation still occurs on the GPU; .get() is the host synchronization and transfer.

Public introspection

Useful read-only properties include:

Property Meaning
name, fullName, fullNameWithHash User and generated identifiers
rows, cols, size Per-instance shape
correspondance Owning hierarchy object
through Connectivity for JOIN-like nodes
children, operator Symbolic graph structure
value Current PyCUDA buffer
isDynamic Whether correspondence is a dynamic primitive
isZero, isIdentity, isFloatMat Symbolic simplification classification
hash Structural identity used for caches

deviceKernel, globalKernel, generate_code, disable_array_access, setName, and setAsIntermediate are exposed for generator work. Normal model construction should use hierarchy methods instead.

Lineage errors

If operands belong to unrelated primitives, arithmetic raises an error. Define the relationship with JOIN or UNION. Copying numerical buffers to bypass lineage would remove the differentiation and sparse-index path that YASPS needs.