Overview

This free online book marks our commitment to make the theory and algorithms of physics-based simulations accessible to everyone.

Contributing

If you are interested in contributing to editing and improving this book, please do it through a Github pull request on the mdbook-src repository (not the HTML repository), or directly contact Minchen Li and Chenfanfu Jiang.

Depending on the nature of your contribution, you'll be listed as book co-authors or community contributors in future builds of the book.

Version 1.0.1 (Released 2024/8):

Co-authors

Community Contributors (Github)

Minchen Li (@liminchen), Chenfanfu Jiang (@cffjiang), Zhaoming Xie (@zxie-bdai), Yuqi Meng (@ARessegetesStery), Zhaofeng Luo (@Roushelfy)

BibTeX

@book{li2024physics,
  title={Physics-Based Simulation},
  author={Minchen Li and Chenfanfu Jiang and Zhaofeng Luo},
  year={2024},
  url={https://phys-sim-book.github.io/}
}

Lecture 1: Discrete Space and Time

In this lecture, we explore the simulation of deformable solids with the aim of developing a discrete, computationally solvable problem. The primary goal is to introduce the abstract algebraic concepts inherent in this problem. We approach elasticity simulation using a top-down architectural view, placing mathematical modeling at the forefront.

The study of classical elastic solids physics largely revolves around Partial Differential Equations (PDEs). In continuum mechanics and finite element analysis literature, the norm is to first derive the continuous form of these PDEs, elaborating on each term's origin, before adapting them to discrete programming languages. Often, this adaptation appears in later sections, creating a sense of anticipation for the reader.

This book, however, takes a different route. It weaves continuum mechanics and PDEs into the discussion as needed, evenly distributing these topics to avoid overwhelming the reader. This method links theory to practice incrementally, enhancing understanding.

We introduce the main problem formulation early, offering an overview of its numerical solutions. This gives readers an initial comprehensive view, sparking curiosity and motivating deeper exploration in later chapters. This strategy makes the learning process smoother and more intuitive, helping readers effortlessly connect complex concepts and quickly grasp the subject's core.

Our aim is to provide a well-rounded, thorough, and engaging exploration of deformable solids simulation, valuable for both students and seasoned researchers in the field.

Representations of a Solid Geometry

In everyday life, solid objects are perceived as continuous. Yet, in the digital world of computers, where we use discrete numbers for representation, a range of interesting methods arises.

One method is parametrization. Consider a 3D sphere, which can be described as , centered at point \( \mathbf{c} \) with radius \( r \). This approach extends beyond spheres to include shapes like half-spaces, boxes, ellipsoids, tori, and others, characterized by their interior using functions such as signed distances. However, parametrization faces challenges when handling complex geometries that are frequently encountered in real-world scenarios. An emerging exception to this limitation is the use of advanced neural representations employing neural networks. These newer methods show promise in effectively representing more intricate geometrical forms.

An alternative is representing with sampling. This involves choosing points on and inside the object. But points alone aren't enough; we typically need to establish connectivity between them to define the object’s boundaries for applications like rendering and 3D printing. Monitoring how a cluster of points shifts over time also helps in measuring deformation.

In continuum mechanics, an object is seen as having a continuous density field. Digitally, this continuity must be represented discretely, usually through defining the connectivity of the solid's geometry.

Remark 1.1.1 (Other Solid Representations). There are other methods for representing solid geometries, such as voxel-based approaches. These methods divide the space into a 3D grid of small boxes, or voxels, with each voxel representing a segment of the object, similar to pixels in a 2D image. Voxel-based methods are advantageous for several reasons. Firstly, they can act as a discrete level set representation, capable of modeling complex geometries and tracking their evolution over time. Each voxel contains information about its position relative to the object's surface, offering an efficient discrete approximation of the continuous level set function. This is beneficial for algorithms involved in surface evolution, shape optimization, and collision detection. Secondly, voxel-based approaches are conducive to Constructive Solid Geometry (CSG) operations. This technique in solid modeling uses Boolean operators to combine simpler shapes into complex 3D models. The voxelized framework allows for straightforward and efficient execution of operations like union, intersection, and difference on the voxel grid. This enables the easy creation and modification of intricate shapes.

Example 1.1.1 (Mesh). The method of creating a mesh by directly connecting points with edges or triangles is a popular technique in computational geometry. This concept is illustrated in the accompanying figure, where the left and middle images show two different meshes. Notably, even though these meshes utilize the same sampled points or nodes, they have distinct connectivities, resulting in different shapes. The rightmost mesh in the figure demonstrates a transformation from one shape to another. This mesh represents a deformation of the middle mesh, achieved by vertically compressing its upper half.

Figure 1.1.1. Mesh

Example 1.1.2 (Particle and Grid). By implementing a uniform grid structure in our spatial representation, we record the extent of solid matter at each node location. This allows us to use our sampled points to calculate the density of the solid at each grid node. This method is beneficial for quantifying the solid's distribution within the grid and for establishing a network of connectivity among the original sampled points. Refer to the accompanying figure for a visual demonstration of this concept. In the figure, the sampled points are depicted as green dots. The grid nodes, where we record solid densities, are shown as black circles. These nodes are connected through the grid, illustrated with blue lines.

Figure 1.1.2. Particle and grid

In the field of modern solid simulation, the described methods of defining connectivity are crucial. The first method, establishing connections through a mesh of edges or triangles, is foundational to Finite Element Method (FEM) simulators. The second approach, which involves using a uniform grid to compute solid density and establish connectivity, is integral to Material Point Method (MPM) simulators [Jiang et al. 2016]. This book largely concentrates on the former method, delving into the intricacies of FEM. The mesh-based structure of FEM is particularly effective in handling complex domains by breaking them down into simpler elements. This makes FEM an essential tool in the study and simulation of deformable solids, and understanding its nuances is vital for those engaged in this area of study.

At first glance, the use of two representations of solid geometry in the MPM might appear redundant. Yet, this dual approach gives MPM a significant edge, especially in simulating dynamic events like solid fractures. In such cases, FEM would necessitate meticulous modification of the edges and elements that define the original connectivity to accurately depict the damage. In contrast, MPM efficiently handles these scenarios. The uniform grid naturally accommodates the separation of body parts in a fracture, as the lack of material at fracture nodes leads to an automatic disconnection of adjacent grid nodes. This attribute allows MPM to excel in managing changes in solid topology.

However, when it comes to simulation accuracy control, the Finite Element Method (FEM) excels. FEM operates directly on the mesh, obviating the need for constant information transfer, thus ensuring greater precision. This level of accuracy makes FEM an invaluable resource in the precise simulation of deformable solids, which is the primary emphasis of this book.

The technique of consolidating coordinates of each sampled point into an extended vector, denoted as \( x\in\mathbb{R}^{dn} \) (refer to the figure below), provides an effective means to describe a specific geometric configuration, given a constant connectivity. In this representation, \(d\) indicates the dimension of space (1, 2, or 3), and \(n\) represents the total number of points. Similarly, attributes like velocity, acceleration, and forces at each sample point can be amalgamated into corresponding extended vectors, symbolized as \(v\), \(a\), and \(f\) respectively. This organized approach to data presentation not only aids in comprehensively understanding the various parameters and their interrelations but also streamlines the mathematical formulation of the simulation process.

Figure 1.1.3. Stacked position vector

Newton's Second Law

Having defined a method for representing a solid geometry at a single instance in time, we now face the challenge of predicting the solid's motion and deformation over time. This prediction is a key component for accurate simulation.

Newton's second law, expressed as \(\mathbf{f} = m \mathbf{a}\), indicates that forces \(\mathbf{f}\) are the primary reasons for changes in velocity, as indicated by acceleration \(\mathbf{a}\). It's important to understand that when a solid's displacement fields extend beyond simple translational or rotational movements, or a linear combination thereof, it indicates deformation. By applying Newton's second law to each sample point, we can effectively predict the movement and deformation of solids. This concept is concisely represented in vector form:

In this representation, \(M\in\mathbb{R}^{dn\times dn}\) is the mass matrix, and \(x\), \(v\), and \(f\) are the column vectors stacking position, velocity, and force, respectively. This approach lays the groundwork for our simulations of deformable solids, integrating principles of motion in both discrete space and continuous time.

Remark 1.2.1 (Stacked Variables). Though the mass matrix \(M\) isn't necessarily a diagonal matrix in theory, it's often simplified to one in practical applications. This results in a lumped mass matrix, representing a system of discrete point masses and offering an efficient way to handle complex systems. Consider a two-point system in two dimensions to illustrate this. The lumped mass matrix for such a system is represented as: \[ M = \begin{pmatrix} m_1 & & & \\ & m_1 & & \\ & & m_2 & \\ & & & m_2 \end{pmatrix}, \] Here, we assume vectors like \({v}\) (as well as \({x}\) and \({f}\)) are stacked in a specific order: \[ v = (v_{11}, v_{12}, v_{21}, v_{22})^T, \] where \(v_{i\alpha}\) denotes the \(\alpha\)th component of the velocity \(\mathbf{v}_i\) for the \(i\)th point. Such an organized structure simplifies calculations significantly and enhances the understanding of the system's dynamics.

Time Integration

Newton's second law lays the foundation for a series of Ordinary Differential Equations (ODEs) expressed in their continuous forms. This is analogous to how we previously used sampled points in space to discretely represent continuous geometries. Now, we take a similar approach but in the realm of time. By sampling points in time, we can effectively represent time derivatives, such as \(\frac{\mathbf{d} x}{\mathbf{d} t}\) and \(\frac{\mathbf{d} v}{\mathbf{d} t}\).

Definition 1.3.1 (Time Integration). When discretizing time into fixed small intervals, we denote the time at the \(n\)-th step as \(t^n\), commonly referred to as a timestep. The length of this interval, or timestep size, is given by \(\Delta t = t^{n+1} - t^n\). The timestep count, \(n\), is typically a whole number starting from zero, making \(t^0=0 s\) the starting point of a simulation.

The concept of timesteps leads to the introduction of symbols \(x^n\), \(v^n\), and \(f^n\) to represent the positions, velocities, and forces of nodes at the \(n\)-th timestep, respectively. The term timestepping, or time integration, refers to the process of calculating \(x^{n+1}, v^{n+1}\) from \(x^n, v^n\) at each incremental timestep \(n=0,1,2,\dots\). For a visual demonstration, consider an Armadillo slingshot animation. Each frame in this animation is computed progressively from left to right, as illustrated in the figure below. In this context, timestepping mirrors a cinematic progression, revealing the evolving dynamics of a system in a step-by-step manner.

Figure 1.3.1. Armadillo slingshot frame by frame

In the context of this book and the simulation scenarios we examine, a crucial assumption must be emphasized: we always possess exact knowledge of the initial values \(x^0\) and \(v^0\) at the start of our simulation. Furthermore, for each timestep, we either have a method to calculate \(f^n\) based on a physical model, or we have its precise value readily available, as with a constant force such as gravity. This assumption is fundamental to our approach, ensuring that simulations are grounded in known initial conditions and forces, thereby allowing for more accurate and reliable outcomes.

Explicit Time Integration

Explicit time integration schemes provide a direct method to calculate \(x^{n+1},v^{n+1}\) by substituting known values into simple formulas, which is why these are called explicit. This section focuses on two basic explicit schemes: Forward Euler and Symplectic Euler methods.

Forward Euler

To convert our continuous-time system to a discrete form, we employ the forward difference approximation. In this approximation, the derivative \((\frac{\mathbf{d} x}{\mathbf{d} t})^n\) is estimated as \(\frac{x^{n+1} - x^n}{\Delta t}\), and likewise, \((\frac{\mathbf{d} v}{\mathbf{d} t})^n\) as \(\frac{v^{n+1} - v^n}{\Delta t}\). The superscript \(n\) represents the state variables at the \(n\)th timestep. Consequently, the discrete version of our system is expressed as: Assuming a constant mass over time, these equations provide a clear mechanism to update our state variables. Knowing the current values \(x^n\), \(v^n\), and \(f^n\) at timestep \(n\), we can directly determine their values at the next timestep, \(n+1\).

Method 1.4.1 (Forward Euler Time Integration for Newton's Second Law). In the Forward Euler method, the state variables \(x^{n+1}\) and \(v^{n+1}\) at the next time step \(n+1\) are calculated based on the current values \(x^n\) and \(v^n\). The update rules are given by: Here, \(\Delta t\) represents the time step size, \(M\) is the mass matrix, and \(f^n\) is the force at the current time step \(n\).

The forward Euler method is considered unconditionally unstable, implying that irrespective of the chosen small time step \(\Delta t\), the numerical solution will eventually grow significantly (explode) for equations with nonconstant \(f\), while the exact solution remains unaffected (refer to Figure 1.4.1, left).

Symplectic Euler

If we put superscript \(n+1\) on \(v\) in the position derivative discretization while keeping the velocity derivative the same, we get a new update rule:

Method 1.4.2 (Symplectic Euler Time Integration for Newton's Second Law). Given the current state variables, the mass matrix, and the time step size from \(t^n\) to \(t^{n+1}\), where \(n=0,1,2,\dots\).

With a minor alteration, the integration becomes conditionally stable. This implies that if \(\Delta t\) remains within a problem-specific limit, we can effectively confine the numerical error of the solution. Moreover, the Symplectic Euler method exhibits an appealing trait of system energy preservation, as demonstrated in the middle of the figure below.

Figure 1.4.1 (Stability of Time Integrators). The provided illustration showcases a particle executing constant circular motion, simulated using the forward Euler, Symplectic Euler, and implicit Euler methods, respectively from left to right. The varying colors within the illustration represent the progression of time. Notably, each method exhibits distinct characteristics in the simulation: the forward Euler simulation eventually undergoes an unstable escalation, the Symplectic Euler closely adheres to the theoretical trajectory, and the implicit Euler, while maintaining stability, gradually brings the motion to a halt.

Implicit Time Integration

In contrast to explicit time integration, implicit time integration requires solving a system of equations to determine the values of \(x^{n+1}\) and \(v^{n+1}\). A notable benefit of this approach is its potential for greatly improved stability. The simplest form of implicit integration, the backward Euler method, is introduced as follows.

Method 1.5.1 (Backward Euler Time Integration Application to Newton's Second Law). Given the current state variables, the mass matrix, and the time interval from \(t^n\) to \(t^{n+1}\), the update rules are as follows: where \(n\) ranges from \(0,1,2,\dots\).

In many scenarios discussed in this book, the forces are derived from position vectors \(x\). Thus, we can represent \(f^{n+1} = f(x^{n+1})\). It's crucial to recognize that the update for \(x^{n+1}\) depends on knowing \(v^{n+1}\), yet the calculation of \(v^{n+1}\) is contingent on \(x^{n+1}\). This interdependence creates a cyclical dependency, necessitating the resolution of a system of equations to accurately find \(x^{n+1}\) and \(v^{n+1}\). By formulating \(v^{n+1} = (x^{n+1} - x^n) / \Delta t\), Equation (1.5.1) can be rephrased as: Given that forces \(f\) often exhibit nonlinearity with respect to positions \(x\), Equation (1.5.2) generally becomes nonlinear, requiring the use of nonlinear root finding techniques like Newton's method for solution.

Method 1.5.2 (Newton's Method Applied to Backward Euler Time Integration). As described in the algorithm below, Newton's method is an iterative technique starting from an initial estimate \(x^i\) of the solution. At the current iteration \(x^i\), it linearly approximates \(f(x^{n+1}) \approx f(x^i) + (x^{n+1}-x^i) \nabla f(x^i)\), then resolves a linear system and updates the iteration. This process is repeated until a satisfactory degree of convergence is reached.

Algorithm 1.5.1 (Newton's Method for Backward Euler Time Integration).

While the backward Euler method ensures unconditional stability even for large values of \(\Delta t\), it's crucial to recognize that increasing \(\Delta t\) may lead to poorer system conditioning. This complication can make solving the linear system more challenging. Additionally, it's important to remember that force linearization is an approximation. If the initial estimate for the solution is far from the actual solution, the standard iteration of Newton's method might not converge, and it could even diverge.

In later discussions, we will introduce a modified version of Newton's method. This adaptation is designed to guarantee convergence for specific types of problems, regardless of the initial estimate or the size of \(\Delta t\).

Summary

Simulating solids involves predicting changes in their position and form over time. To achieve this on computers, both geometry and time must be represented discretely.

Geometries are typically represented using sample points interconnected in specific ways:

  • Finite Element Methods (FEM) connect sample points through unstructured meshes.
  • Material Point Methods (MPM) utilize uniform Cartesian grids to link sample points. FEM excels in delivering high-precision results, while MPM is advantageous for handling topological changes. This book primarily focuses on FEM.

Time is discretized into distinct moments, with finite difference methods applied to calculate temporal derivatives of physical quantities, in line with Newton's second law.

The Forward Euler method is generally avoided due to its unconditional instability. Conversely, the Symplectic Euler method is explicit and conditionally stable, often preferred for well-conditioned problems. For stiff problems, the Backward Euler method, unconditionally stable but requiring the resolution of nonlinear equation systems, is commonly used despite its computational intensity and potential for numerical instability.

In the next lecture, we will explore the optimization perspective of implicit time integration, offering robustness in solving these problems.

Lecture 2: Optimization Framework

Optimization Time Integrator

With the backward Euler method, each timestep necessitates solving a nonlinear system of equations, as outlined in Equation (1.5.2). Effectively, this equates to addressing an optimization problem stated as: Here, \(\tilde{x}^n = x^n + \Delta t v^n\), \(\frac{1}{2} \|x - \tilde{x}^n\|^2_M = \frac{1}{2} (x - \tilde{x}^n)^T M (x - \tilde{x}^n)\) represents the inertia term, \(P(x)\) stands for the potential energy for forces \(f(x)\) with \(\frac{\partial P}{\partial x}(x) = -f(x)\), and \(E(x)\) is known as the Incremental Potential. At the local minimum of \(E(x)\), \(\frac{\partial E}{\partial x}(x^{n+1}) = 0\), corresponding to Equation (1.5.2).

Viewing time integration as an optimization problem enables us to utilize well-established optimization methods to robustly acquire the solutions. It also allows for a consistent framework for modeling more complex physical phenomena.

Definition 2.1.1 (Conservative Forces). Forces \(f(x)\) for which a potential energy \(P(x)\) exists such that \(\frac{\partial P(x)}{\partial x} = -f(x)\), are termed conservative forces. Both common elasticity forces and body forces such as gravity are examples of conservative forces. They can be easily integrated into the optimization framework by adding the potential energy term into the Incremental Potential.

Remark 2.1.1 (The gravitational force). The gravitational force acting on an object of mass \(m\) (represented by the force \(F = -mg\mathbf{z}\)) at a height \(h\) above the Earth's surface, where \(g\) is the acceleration due to gravity and \(\mathbf{z}\) is the upward-pointing unit vector, corresponds to the gravitational potential energy \(U = mgh\). Here, \(U\) is the work done against gravity to move the object from a reference point (at \(h = 0\)) to height \(h\). The force is the negative gradient of the energy with respect to the position (written mathematically as \(F = -\nabla U\)), which confirms the principle of conservation of energy. Taking the derivative of \(U\) with respect to \(h\), we obtain \(\nabla U = mg\mathbf{z}\), and thus \(F = -\nabla U = -mg\mathbf{z}\), which matches our starting expression for the force.

Remark 2.1.2 (Elasticity). Elasticity is the capacity of a solid object to maintain its resting shape in response to external forces. Under the influence of elasticity, the sample points on the same solid will be bound together during the simulation. A more rigid solid will have a stiffer elasticity energy, providing a larger elasticity force for the same degree of deformation, thereby aiding in the restoration of the resting shape. The Armadillo slingshot example (Figure 1.3.1) demonstrates typical elasticity effects. Elasticity is a common property across all solids, regardless of their geometric form, and whether they are intuitively rigid or non-rigid, e.g., metal, wood, soft tissue, rubber, cloth, hair, sand, etc.

Dirichlet Boundary Conditions

Potential energies aren't the only means of modeling physical phenomena; constraints are equally vital. Let's start by considering the simplest form, linear equality constraints. The constrained optimization problem is defined as follows: Here, \(A\in \mathbb{R}^{m\times dn}\) and \(b\in \mathbb{R}^{m}\) represent \(m\) linear equality constraints.

During simulations, it's often necessary to control the position of certain points on a solid at each timestep. This can involve fixing a set of nodes to model immovable objects like the ground or obstacles, or guiding the motion of solids by moving specific nodes along predetermined paths. For example, in the slingshot scenario (Figure 1.3.1), the Armadillo's feet and ears are stationary. This type of control is known as Dirichlet boundary conditions (BC). These conditions can be expressed as linear equality constraints within the optimization time integrator framework.

To put it into perspective, the matrix \(A\) in Equation (2.2.1) would typically be an \(m\times dn\) matrix (with \(m\leq dn\) and \(m \mod d = 0\)), which selects the coordinates of the BC points. Correspondingly, \(b\) would be an \(m \times 1\) vector defining the prescribed locations. By solving the optimization problem, the chosen points are fixed at these specified locations, which can vary from one timestep to the next.

At the local minimum of the problem in Equation (2.2.1), the KKT condition is met, where \(\lambda \in \mathbb{R}^{m}\) represents the Lagrange multiplier vector, comprising all the Lagrange multipliers.

Remark 2.2.1 (Solving KKT Systems). Solving nonlinear optimization problems with equality constraints is feasible by directly addressing the nonlinear KKT (Karush-Kuhn-Tucker) system, as seen in Equation (2.2.2). Methods like Newton's method are commonly employed for this purpose. However, this approach can be computationally intensive. For boundary conditions, the unique structure of the matrix \(A\) can be leveraged, allowing us to resolve the constrained problem in an unconstrained manner. Techniques for this approach will be demonstrated in later lectures.

Contact

To accurately simulate solids, it's essential to ensure that they don't interpenetrate, as shown in the figure below (left side). One effective approach is to enforce the CFL (Courant-Friedrichs-Lewy condition) upper limit on timestep sizes, particularly in methods like MPM. In Finite Element Methods (FEM), this requires precise modeling of contact forces. However, accurately modeling contact poses a challenge. Contact is inherently a non-smooth process, happening abruptly as solids make contact. There isn't a potential energy formulation that can accurately depict this phenomenon.

Figure 2.3.1 (Simulation Examples of Contact and Friction). On the left, an intriguing simulation shows four characters plunging into a funnel and then being extruded by a moving plane. The flawless execution, marked by the absence of any interpenetration during this complex interaction, highlights the precision of the models employed. On the right, we see a simulation of the classic table cloth trick, executed at varying speeds. The realism in this simulation, especially the accurate depiction of friction, becomes apparent as the cloth is pulled away without disturbing the table setting — mirroring what one would expect in real life. These simulations showcase the incredible capabilities and precision of contemporary computational models in simulating contact, vividly and engagingly bringing abstract physical concepts to life.

In practical applications, determining if two objects have collided typically involves visually and mentally assessing their proximity. When the distance between them isn't zero, it indicates that space remains and no collision has occurred. This concept is crucial in modeling interactions between objects in a computational context.

To avoid collision or penetration, we can ensure that the distance between the surfaces of the moving objects never reduces to zero. This approach is particularly useful in time integration problems within computational simulations. We model this scenario using inequality constraints, which, when combined with boundary conditions, formulate our time integration problem as follows: Here, \(c_k\) measures the distance between specific pairs of regions on the surface of the solids, and \(\epsilon \rightarrow 0\) is a tiny positive value to ensure \(c_k(x)\) remains strictly positive.

At the local minimum of the problem in Equation (2.3.1), we adhere to the Karush-Kuhn-Tucker (KKT) condition, as follows: In this condition, \(\gamma_k\) is the Lagrange multiplier for the constraint \(c_k(x) \geq \epsilon\). To break it down, \(\nabla c_k(x)\) points in the direction of the contact force for contacting pair \(k\). The combination of this direction with the magnitude represented by \(\gamma_k\) gives us the actual contact force at that point.

Remark 2.3.1 (The Complementarity Slackness Condition). The complementarity slackness condition \(\gamma_k (c_k(x) - \epsilon) = 0\) plays a critical role in ensuring that contact forces are present (\(\gamma_k \neq 0\)) exclusively when the solids are in touch (\(c_k(x) = \epsilon\)). On the contrary, when the solids are not touching (\(c_k(x) > \epsilon\)), there should be no contact forces (\(\gamma_k = 0\)).

Definition 2.3.1 (Active Set). In optimization problems with inequality constraints defined as \[ \forall k, \ c_k(x) \geq 0, \] the active set is defined as \[ \{ l \ | \ c_l(x^*) = 0 \}. \] Here, \(x^*\) is a local optimal solution of the problem.

Remark 2.3.2 (Combinatorial Difficulty). The complementarity slackness condition reveals that only constraints within the active set will exhibit non-zero Lagrange multiplier \(\gamma_k\) at the solution. This suggests that, unlike equality constraints, inequality constraints not only require solving for the value of the Lagrange multipliers but also demand the identification of which \(\gamma_k\) should be set to \(0\). This presents a combinatorial difficulty.

A wide array of techniques are available for addressing optimization problems with inequality constraints. Each method introduces a distinct approach, effectively targeting various facets of the problem.

  • Primal-Dual Methods: This class of methods tackles both the primal problem (the original optimization problem) and its dual problem simultaneously. The dual problem often provides valuable insights into the primal problem's solution, making this approach attractive. These methods are iterative, refining an initial solution by leveraging the relationship between the primal and dual problems. However, designing and implementing primal-dual algorithms can be intricate, requiring a careful balance between the two problem types. While effective, these methods may not be efficient or straightforward for complex, high-dimensional problems.

  • Projected Steepest Descent Methods: A modification of the classic steepest descent method, these methods address constraints. At each iteration, the algorithm moves in the steepest descent direction, then projects back onto the feasible set if it deviates due to constraints. This method's simplicity and straightforwardness make it popular, but it may struggle with ill-conditioned problems where convergence is slow, or with constraints that are challenging to project onto.

  • Interior-Point Methods: Also known as barrier methods, these techniques introduce a barrier function that penalizes infeasible solutions, thereby steering the solution towards the feasible region's interior. This approach effectively transforms a constrained problem into an unconstrained one, solvable using conventional techniques. However, the barrier function's choice significantly impacts the method's performance. While efficient for certain problem types, these methods may falter with problems where the feasible region is difficult to define or lacks a simple interior.

While each of these methodologies has its own strengths and weaknesses, our primary focus will be on a robust and accurate contact modeling method, known as Incremental Potential Contact (IPC). IPC distinguishes itself by approximating the contact process with a smooth potential energy. This transformation effectively turns the problem into an unconstrained one, facilitating the application of various efficient and robust optimization techniques. A key feature of IPC is its capability to control the approximation error relative to the non-smooth formulation within a predetermined bound. This characteristic adds a layer of robustness and reliability to the method, making it an especially promising approach for the problem at hand.

Friction

Friction is a crucial element in physical interactions involving movement, often significantly influencing simulation outcomes. Thus, its precise modeling is vital for realistic and reliable simulations. See Figure 2.3.1 on the right for a demonstration of a scenario that requires a precise representation of friction.

One of the most widely adopted models for friction is the Coulomb Friction model. This model hinges on the Maximal Dissipation Principal (MDP), effectively capturing the nonsmooth transition between static and dynamic frictions. Static friction is the force preventing an object from initiating movement, whereas dynamic friction, or kinetic friction, opposes the motion of a moving object. The Coulomb Friction model accurately depicts the critical transition between these two friction types.

In the standard Material Point Method (MPM), friction is inherently modeled by the grid. However, this method has its drawbacks, notably an uncontrollable and unrealistically large friction coefficient.

For the Finite Element Method (FEM), friction can be more realistically and controllably represented through an approximated potential energy in the Incremental Potential Contact (IPC) model. This fits well within our optimization time integration framework. By using potential energy to approximate friction, we not only maintain the robustness of the simulation but also gain control over the accuracy of the friction model.

In subsequent lectures, we will delve into the specific techniques and methodologies employed in the IPC model to represent friction forces and their role in enhancing the accuracy and realism of simulations.

Summary

The objective of our discussions so far has been to devise a reliable solution for the unconditional stable implicit time integration problem. We aimed to address the issue of non-convergent solutions arising from truncation errors. We tackled this by reformulating the time integration problem as a minimization problem. This formulation not only allowed us to apply well-established optimization techniques, but it also facilitated a consistent modeling framework for different physical phenomena.

Here is a quick summary of the techniques used for modeling various phenomena within this framework:

  • For conservative forces like gravity and elasticity, we used potential energies. These were integrated into the objective function to create an accurate representation of the forces involved.
  • Boundary conditions, which specify the constraints on the system, were modeled using simple linear equality constraints. This helped us restrict the system to feasible states while performing the simulation.
  • To prevent interpenetration between solid objects during the simulation, we used inequality constraints to model contact and friction. These constraints ensured that objects maintained their physical integrity and behaved as expected when they came in contact with each other.

An important aspect to note here is that, we can utilize the unique structure of the boundary conditions to enforce the equality constraints in an unconstrained way. This will lead to a significant reduction in computational complexity.

Moreover, we introduced the concept of the Incremental Potential Contact (IPC) method. The IPC method models contact and friction as smooth potential energies with a controllable level of accuracy. This ensures a robust and accurate simulation of solid objects, free from interpenetration.

Moving forward, in the next lecture, we will delve into the projected Newton method for solving unconstrained optimization problems. This method offers the advantage of global convergence, meaning that the method is guaranteed to converge regardless of the initial configuration, provided it is feasible. This feature is highly desirable for complex simulations and it helps make the method more robust and reliable.

Projected Newton

Convergence Issue of Newton's Method

In addressing the minimization problem presented by implicit Euler time integration (referenced in Equation (2.1.1)), employing Newton's method (outlined in Algorithm 1.5.1) is a viable strategy for the resultant system of nonlinear equations. This involves setting the gradient of the Incremental Potential Energy to zero:

However, the application of this method to cases such as nonlinear elasticity, particularly in the Neo-Hookean model, does not always guarantee convergence. The presence of truncation errors, especially in scenarios involving large time steps or significant deformations, can adversely affect the convergence process.

Example 3.1.1 (Illustration of Newton's Convergence Issue). To elucidate the issue of Newton's method non-convergence, let's consider a one-dimensional minimization problem characterized by the objective function: We can evaluate the function at and approximate it using a quadratic energy , which is defined as: The joint plot of and (Figure 3.1.1) distinctly exhibits that the next iteration would exceed the actual target, landing at a point () further from the actual solution at . The subsequent iterations amplify this deviation, leading to a trajectory that diverges. It's worth noting that this demonstration involves a convex function . The problem can become even more complex when Newton's method is applied to non-convex elasticity energies.

Figure 3.1.1. An iteration of Newton's method for at .

Remark 3.1.1 (Convexity of Energies). Convex functions are characterized by symmetric and positive-definite (SPD) second-order derivatives throughout their domain. Conversely, the energy in most models of nonlinear elasticity used in computer graphics is rotation invariant. This implies that the energy value remains unchanged regardless of the rotational orientation of objects or elements. Such rotation invariance leads to non-convexity, making the optimization process more complex.

Definition 3.1.1 (Symmetric Positive-Definiteness). A square matrix is symmetric positive-definite if

  • , and
  • for all .

Unlike directly solving nonlinear equations, a minimization problem provides an energy measure that enables the assurance of global convergence using a technique called line search.

In iterative minimization methods, line search is a technique used to select a fraction of the step in each iteration, ensuring the objective energy decreases at the new point.

Specifically, for Newton's method, line 4 in Algorithm 1.5.1 is modified from \(x^i \leftarrow x\) to \(x^i \leftarrow x^i + \alpha (x - x^i)\), where \(\alpha \in (0,1]\) is the step size, essential for the reduction of energy. This leads to two critical questions: Does such an \(\alpha\) always exist? And how is \(\alpha\) calculated?

Remark 3.2.1 (Existence of \(\alpha\)). For a smooth objective energy \(E(x)\) at \(x^i\) where \(\nabla E(x^i) \neq 0\), if a search direction \(p=x-x^i\) is descent, namely \(p^T \nabla E(x^i) < 0\), then there exists \(\alpha > 0\) such that \(E(x^i + \alpha p) < E(x^i)\).

Method 3.2.1 (Backtracking Line Search). Given a descent direction, we can find a reasonably large \(\alpha\) by simply halving it starting from \(1\) until the energy at the new location is smaller than the current (see Algorithm 3.2.1).

Algorithm 3.2.1 (The Backtracking Line Search Algorithm).

Remark 3.2.2 (Other Line Search Methods). There are other line search methods that attempt to apply polynomial interpolations to find an \(\alpha\) such that the energy at the new location is closer to a local minimum on the line segment \(x^i + s p\), (\(s\in(0,1]\)). However, these methods generally incur higher computational costs and may not necessarily enhance the overall wall-clock timing of the optimization.

Now, with line search, if Newton's method consistently generates a descent search direction, then the method is guaranteed to converge for any initial configuration on any smooth energy with a lower bound. We know that in iteration \(i\), \(p = -(\nabla^2 E(x^i))^{-1} \nabla E(x^i)\), so \(p^T \nabla E(x^i)\) equals \(-\nabla E(x^i)^T (\nabla^2 E(x^i))^{-T} \nabla E(x^i)\). For convex energies, \(\nabla^2 E(x^i)\) is always Symmetric Positive Definite (SPD), and so is \((\nabla^2 E(x^i))^{-T}\), making \(p\) always a descent direction. However, for non-convex energies, this assurance does not always hold. One approach to address this issue is to approximate the energies locally using convex energy proxies.

Gradient-based Optimization

The search direction of the standard Newton's method is calculated by minimizing the local quadratic approximation of the objective energy: where \(P = \nabla^2 E(x^i)\). In general gradient-based optimization methods, \(p\) can be calculated by Equation (3.3.1) with any proxy matrix \(P\). Setting \(P = I\) results in \(p = -\nabla E(x^i)\), as used in the standard gradient descent method. This approach converges more slowly than Newton's method, as the energy approximation is of a lower order. The closer the proxy matrix \(P\) is to the Hessian matrix \(\nabla^2 E(x^i)\), the faster the convergence. Hence, using an SPD approximation of the Hessian matrix as the proxy ensures that the search direction is always descent, while maintaining a convergence rate close to quadratic. This is akin to approximating non-convex energies locally using a convex energy proxy.

A straightforward method to obtain such an SPD approximation involves first projecting \(\nabla^2 E(x^i)\) onto its closest semi-definite matrix by solving and then introducing perturbations to ensure that \(P\) is invertible. The solution in this case is \(P = Q \hat{\Lambda} Q^{-1}\), where \(P = Q \Lambda Q^{-1}\) is the eigendecomposition, and if \(\Lambda_{ij} > 0\), otherwise \(\hat{\Lambda}_{ij} = 0\). Intuitively, \(P\) is obtained by zeroing out all the negative eigenvalues of \(\nabla^2 E(x^i)\).

Definition 3.3.1 (Eigendecomposition). The eigendecomposition of a square matrix \(A \in \mathbb{R}^{n \times n}\) is where \(Q = [q_1, q_2, ..., q_n]\) is composed of the eigenvectors \(q_i\) of \(A\), ; \(\Lambda = [\lambda_1, \lambda_2, ..., \lambda_n]\), with \(\lambda_1 \geq \lambda_2 \geq ..., \lambda_n\) being the eigenvalues of \(A\); and \(Aq_i = \lambda_i q_i\).

However, in simulation, \(\nabla^2 E(x^i)\) is usually a large sparse matrix, and performing eigendecomposition on it would be prohibitively expensive. Fortunately, we will discover later in this book that the Incremental Potential in solids simulation can be expressed as a separable sum of energies defined on local stencils, such as a triangle in the 2D Finite Element Method (FEM) mesh: where \(\mathbf{x}_{jk}\) are the nodes associated with the energy \(E_j\). Consequently, we can conveniently obtain a reasonably good SPD approximation by zeroing out the negative eigenvalues of each \(\nabla^2 E_i\) defined on a small number of nodes and aggregating them.

Example 3.3.1 (Local Projection Method). To simulate elasticity in 2D on a triangle mesh with 10,201 nodes and 20,000 triangles, the Hessian matrix \(\nabla^2 E(x)\) is \(20,402 \times 20,402\). For the local projection method described above, it requires 20,000 eigendecompositions on \(6 \times 6\) matrices. Considering the computational complexity of eigendecomposition on an \(n \times n\) matrix is worse than \(O(n^2)\), this rough estimation already suggests a more than \(500\times\) speedup for this medium-sized problem when employing the local projection methods.

In addition, since the mass matrix in \(\nabla^2 E(x^i)\) is Symmetric Positive Definite (SPD) and the sum of SPD matrices remains SPD, there is no need for perturbations when projecting other matrices. We now summarize the globally convergent projected Newton method for backward Euler time integration in Algorithm 3.3.1.

Algorithm 3.3.1 (Projected Newton Method for Backward Euler Time Integration).

Remark 3.3.1 (Stopping Criteria). From Equation (3.3.1), we understand that can be interpreted as a quadratic approximation of the distance from the current estimate \(x^i\) to the optimal solution. Hence, we utilize as a more intuitive measure for the stopping criteria. This approach transforms it into a velocity unit and takes the maximum magnitude across every node.

Summary

  • After examining the convergence issues of traditional Newton's method, even on smooth convex energies, we introduced a backtracking line search scheme for minimizing the Incremental Potential of Implicit Euler time integration, ensuring global convergence.
  • To guarantee the discovery of a positive step size, the Incremental Potential Hessian is projected onto a nearby Symmetric Positive Definite (SPD) matrix. This SPD projection is efficiently achieved by eliminating the negative eigenvalues of the Hessian matrices for each non-convex energy stencil, involving only a few nodes.
  • A convergence criterion that provides a more intuitive and consistent method for setting tolerance is also introduced, utilizing the Newton search direction.

In the next lecture, we will conclude with a clear demonstration of all the covered topics through a simple 2D case study.

Case Study: 2D Mass Spring*

Up to now, we have completed a high-level introduction to the optimization-based solids simulation framework. In this lecture, we elaborate on how to implement a simple 2D elastodynamics simulator with Python3 (CPU) and MUDA (GPU).

Sections in this book with Python CPU and MUDA GPU implementations will be marked with a * right after the title. All the Python and MUDA implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial and https://github.com/phys-sim-book/solid-sim-tutorial-gpu, respectively. The excutable project for this section is in the /1_mass_spring folder of these repositories.

Spatial and Temporal Discretizations

In representing solid geometries, we employ a mesh structure. We can further simplify the representation by connecting nodes on the mesh with edges. To facilitate this process, especially for geometries like squares, we can script a mesh generator. This generator allows for specifying both the side length of the square and the desired resolution of the mesh.

Implementation 4.1.1 (Square Mesh Generation, square_mesh.py).

import numpy as np
import os

def generate(side_length, n_seg):
    # sample nodes uniformly on a square
    x = np.array([[0.0, 0.0]] * ((n_seg + 1) ** 2))
    step = side_length / n_seg
    for i in range(0, n_seg + 1):
        for j in range(0, n_seg + 1):
            x[i * (n_seg + 1) + j] = [-side_length / 2 + i * step, -side_length / 2 + j * step]
    
    # connect the nodes with edges
    e = []
    # horizontal edges
    for i in range(0, n_seg):
        for j in range(0, n_seg + 1):
            e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j])
    # vertical edges
    for i in range(0, n_seg + 1):
        for j in range(0, n_seg):
            e.append([i * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])
    # diagonals
    for i in range(0, n_seg):
        for j in range(0, n_seg):
            e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j + 1])
            e.append([(i + 1) * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])

    return [x, e]

In the code, n_seg represents the number of edges along each side of the square. The nodes are uniformly distributed across the square and interconnected through horizontal, vertical, and diagonal edges. For instance, calling generate(1.0, 4) constructs a mesh as depicted in Figure 4.1.1. This implementation utilizes the array data structures from the Numpy library, which provides convenient methods for handling the vector algebra required in subsequent steps.

Figure 4.1.1. A square mesh generated by calling generate(1.0, 4) defined in Square Mesh Generation script above.

For temporal discretization, our approach is the implicit Euler method. The Incremental Potential, which needs to be minimized in time step \(n\), is represented as follows: Next, our focus shifts to implementing the calculations for the energy value, gradient, and Hessian for both the inertia term and the potential energy \(P(x)\).

Inertia Term

For the inertia term, with \(\tilde{x}^n = x^n + h v^n\), we have \[ E_I(x) = \frac{1}{2}\|x - \tilde{x}^n \|_M^2, \quad \nabla E_I(x) = M(x - \tilde{x}^n), \quad \text{and} \quad \nabla^2 E_I(x) = M, \] which is straightforward to implement:

Implementation 4.2.1 (InertiaEnergy.py).

import numpy as np

def val(x, x_tilde, m):
    sum = 0.0
    for i in range(0, len(x)):
        diff = x[i] - x_tilde[i]
        sum += 0.5 * m[i] * diff.dot(diff)
    return sum

def grad(x, x_tilde, m):
    g = np.array([[0.0, 0.0]] * len(x))
    for i in range(0, len(x)):
        g[i] = m[i] * (x[i] - x_tilde[i])
    return g

def hess(x, x_tilde, m):
    IJV = [[0] * (len(x) * 2), [0] * (len(x) * 2), np.array([0.0] * (len(x) * 2))]
    for i in range(0, len(x)):
        for d in range(0, 2):
            IJV[0][i * 2 + d] = i * 2 + d
            IJV[1][i * 2 + d] = i * 2 + d
            IJV[2][i * 2 + d] = m[i]
    return IJV

The functions val(), grad(), and hess() are designed to compute different components of the inertia term. Specifically:

  • val(): Computes the value of the inertia term.
  • grad(): Calculates the gradient of the inertia term.
  • hess(): Determines the Hessian of the inertia term.

Regarding the Hessian matrix, a memory-efficient approach is employed. Rather than allocating a large two-dimensional array to store all entries of the Hessian matrix, only the nonzero entries are kept. This is achieved using the IJV structure, which consists of three lists:

  1. Row Index: Identifies the row position of each nonzero entry.
  2. Column Index: Indicates the column position of each nonzero entry.
  3. Value: The actual nonzero value at the specified row and column.

This method significantly reduces memory usage and computational costs associated with downstream processing.

Mass-Spring Potential Energy

In this case study, we focus exclusively on incorporating the mass-spring elasticity potential into our system. The concept of mass-spring elasticity is akin to treating each edge of the mesh as if it were a spring. This approach is inspired by Hooke's Law, allowing us to formulate the potential energy on edge as follows:

Here, and represent the current positions of the two endpoints of the edge. The variable denotes the original length of the edge, and is a parameter controlling the spring's stiffness. Notably, when the distance between the two endpoints equals the original length , the potential energy attains its global minimum value of , indicating no force is exerted.

An important aspect of this formulation is the inclusion of at the beginning. This is analogous to integrating the spring energy across the solid and choosing edges as quadrature points. This integration helps maintain a consistent relationship between the stiffness behavior and the parameter , regardless of mesh resolution variations.

Another deviation from standard spring energy formulations is our avoidance of the square root operation. We directly use , making our model polynomial in nature. This simplification yields more streamlined expressions for the gradient and Hessian:

The total potential energy of the system, denoted as , can be derived by summing the potential energy across all edges. This is calculated using Equation (4.3.1). Thus, the total potential energy is expressed as: where the summation is taken over all edges in the mesh.

Implementation 4.3.1 (MassSpringEnergy.py).

import numpy as np
import utils

def val(x, e, l2, k):
    sum = 0.0
    for i in range(0, len(e)):
        diff = x[e[i][0]] - x[e[i][1]]
        sum += l2[i] * 0.5 * k[i] * (diff.dot(diff) / l2[i] - 1) ** 2
    return sum

def grad(x, e, l2, k):
    g = np.array([[0.0, 0.0]] * len(x))
    for i in range(0, len(e)):
        diff = x[e[i][0]] - x[e[i][1]]
        g_diff = 2 * k[i] * (diff.dot(diff) / l2[i] - 1) * diff
        g[e[i][0]] += g_diff
        g[e[i][1]] -= g_diff
    return g

def hess(x, e, l2, k):
    IJV = [[0] * (len(e) * 16), [0] * (len(e) * 16), np.array([0.0] * (len(e) * 16))]
    for i in range(0, len(e)):
        diff = x[e[i][0]] - x[e[i][1]]
        H_diff = 2 * k[i] / l2[i] * (2 * np.outer(diff, diff) + (diff.dot(diff) - l2[i]) * np.identity(2))
        H_local = utils.make_PSD(np.block([[H_diff, -H_diff], [-H_diff, H_diff]]))
        # add to global matrix
        for nI in range(0, 2):
            for nJ in range(0, 2):
                indStart = i * 16 + (nI * 2 + nJ) * 4
                for r in range(0, 2):
                    for c in range(0, 2):
                        IJV[0][indStart + r * 2 + c] = e[i][nI] * 2 + r
                        IJV[1][indStart + r * 2 + c] = e[i][nJ] * 2 + c
                        IJV[2][indStart + r * 2 + c] = H_local[nI * 2 + r, nJ * 2 + c]
    return IJV

In dealing with the Hessian matrix of the mass-spring energy, a key consideration is its non-symmetric positive definite (SPD) nature. To address this, a specific modification is employed: we neutralize the negative eigenvalues of the local Hessian corresponding to each edge. This is done prior to incorporating these local Hessians into the global matrix. The process involves setting negative eigenvalues to zero, thus ensuring that the resulting global Hessian matrix adheres more closely to the desired SPD properties. This modification is crucial for Newton's method.

Implementation 4.3.2 (Positive Semi-Definite Projection).

import numpy as np
import numpy.linalg as LA

def make_PSD(hess):
    [lam, V] = LA.eigh(hess)    # Eigen decomposition on symmetric matrix
    # set all negative Eigenvalues to 0
    for i in range(0, len(lam)):
        lam[i] = max(0, lam[i])
    return np.matmul(np.matmul(V, np.diag(lam)), np.transpose(V))

Optimization Time Integrator

Having established the capability to evaluate the Incremental Potential for arbitrary configurations, we now turn our attention to the implementation of the optimization time integrator. This integrator is crucial for minimizing the Incremental Potential, which in turn updates the nodal positions and velocities. This implementation follows the approach outlined in Algorithm 3.3.1:

Implementation 4.4.1 (time_integrator.py).

import copy
from cmath import inf

import numpy as np
import numpy.linalg as LA
import scipy.sparse as sparse
from scipy.sparse.linalg import spsolve

import InertiaEnergy
import MassSpringEnergy

def step_forward(x, e, v, m, l2, k, h, tol):
    x_tilde = x + v * h     # implicit Euler predictive position
    x_n = copy.deepcopy(x)

    # Newton loop
    iter = 0
    E_last = IP_val(x, e, x_tilde, m, l2, k, h)
    p = search_dir(x, e, x_tilde, m, l2, k, h)
    while LA.norm(p, inf) / h > tol:
        print('Iteration', iter, ':')
        print('residual =', LA.norm(p, inf) / h)

        # line search
        alpha = 1
        while IP_val(x + alpha * p, e, x_tilde, m, l2, k, h) > E_last:
            alpha /= 2
        print('step size =', alpha)

        x += alpha * p
        E_last = IP_val(x, e, x_tilde, m, l2, k, h)
        p = search_dir(x, e, x_tilde, m, l2, k, h)
        iter += 1

    v = (x - x_n) / h   # implicit Euler velocity update
    return [x, v]

def IP_val(x, e, x_tilde, m, l2, k, h):
    return InertiaEnergy.val(x, x_tilde, m) + h * h * MassSpringEnergy.val(x, e, l2, k)     # implicit Euler

def IP_grad(x, e, x_tilde, m, l2, k, h):
    return InertiaEnergy.grad(x, x_tilde, m) + h * h * MassSpringEnergy.grad(x, e, l2, k)   # implicit Euler

def IP_hess(x, e, x_tilde, m, l2, k, h):
    IJV_In = InertiaEnergy.hess(x, x_tilde, m)
    IJV_MS = MassSpringEnergy.hess(x, e, l2, k)
    IJV_MS[2] *= h * h    # implicit Euler
    IJV = np.append(IJV_In, IJV_MS, axis=1)
    H = sparse.coo_matrix((IJV[2], (IJV[0], IJV[1])), shape=(len(x) * 2, len(x) * 2)).tocsr()
    return H

def search_dir(x, e, x_tilde, m, l2, k, h):
    projected_hess = IP_hess(x, e, x_tilde, m, l2, k, h)
    reshaped_grad = IP_grad(x, e, x_tilde, m, l2, k, h).reshape(len(x) * 2, 1)
    return spsolve(projected_hess, -reshaped_grad).reshape(len(x), 2)

Here step_forward() is essentially a direct translation of the projected Newton method with line search (Algorithm 3.3.1), and we implemented the Incremental Potential value (IP_val()), gradient (IP_grad()), and Hessian (IP_hess()) evaluations as separate functions for clarity.

For the computation of search directions, we utilize the linear solver from the Scipy library, which is adept at handling sparse matrices. Notably, this solver accepts matrices in the Compressed Sparse Row (CSR) format. The choice of this format and solver is driven by their efficiency in processing and memory usage, which is particularly advantageous when dealing with large-scale problems with large sparse matricies often encountered in computational simulations.

Simulator with Visualization

Having gathered all necessary elements for our 2D mass-spring simulator, the next step is to implement the simulator. This implementation will operate in a step-by-step manner and include visualization capabilities to enhance understanding and engagement.

Implementation 4.5.1 (simulator.py).

# Mass-Spring Solids Simulation

import numpy as np  # numpy for linear algebra
import pygame       # pygame for visualization
pygame.init()

import square_mesh   # square mesh
import time_integrator

# simulation setup
side_len = 1
rho = 1000  # density of square
k = 1e5     # spring stiffness
initial_stretch = 1.4
n_seg = 4   # num of segments per side of the square
h = 0.004   # time step size in s

# initialize simulation
[x, e] = square_mesh.generate(side_len, n_seg)  # node positions and edge node indices
v = np.array([[0.0, 0.0]] * len(x))             # velocity
m = [rho * side_len * side_len / ((n_seg + 1) * (n_seg + 1))] * len(x)  # calculate node mass evenly
# rest length squared
l2 = []
for i in range(0, len(e)):
    diff = x[e[i][0]] - x[e[i][1]]
    l2.append(diff.dot(diff))
k = [k] * len(e)    # spring stiffness
# apply initial stretch horizontally
for i in range(0, len(x)):
    x[i][0] *= initial_stretch

# simulation with visualization
resolution = np.array([900, 900])
offset = resolution / 2
scale = 200
def screen_projection(x):
    return [offset[0] + scale * x[0], resolution[1] - (offset[1] + scale * x[1])]

time_step = 0
square_mesh.write_to_file(time_step, x, n_seg)
screen = pygame.display.set_mode(resolution)
running = True
while running:
    # run until the user asks to quit
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    print('### Time step', time_step, '###')

    # fill the background and draw the square
    screen.fill((255, 255, 255))
    for eI in e:
        pygame.draw.aaline(screen, (0, 0, 255), screen_projection(x[eI[0]]), screen_projection(x[eI[1]]))
    for xI in x:
        pygame.draw.circle(screen, (0, 0, 255), screen_projection(xI), 0.1 * side_len / n_seg * scale)

    pygame.display.flip()   # flip the display

    # step forward simulation and wait for screen refresh
    [x, v] = time_integrator.step_forward(x, e, v, m, l2, k, h, 1e-2)
    time_step += 1
    pygame.time.wait(int(h * 1000))
    square_mesh.write_to_file(time_step, x, n_seg)

pygame.quit()

For 2D visualization in our simulator, we utilize the Pygame library. The simulation is initiated with a scene featuring a single square, which is initially elongated horizontally. During the simulation, the square begins to revert to its original horizontal dimensions. Subsequently, due to inertia, it will start to stretch vertically, oscillating back and forth until it eventually stabilizes at its rest shape, as illustrated in (Figure 4.5.1).

Figure 4.5.1. From left to right: initial, intermediate, and final static frame of the initially stretched square simulation.

In addition to storing node positions x and edges e, our simulation also requires allocating memory for several other key variables:

  • Node Velocities (v): To track the movement of each node over time.
  • Masses (m): Node masses are calculated by uniformly distributing the total mass of the square across each node. This is a preliminary approach; more detailed methods for calculating nodal mass in Finite Element Method (FEM) or Material Point Method (MPM) will be explored in future chapters.
  • Squared Rest Length of Edges (l2): Important for calculating the potential energy in the mass-spring system.
  • Spring Stiffnesses (k): A crucial parameter influencing the dynamics of the springs.

For visualization purposes beyond our simulator, we enable the export of the mesh data into .obj files. This is achieved by calling the write_to_file() function at the start and at each frame of the simulation. This feature facilitates the use of alternative visualization software to analyze and present the simulation results.

Implementation 4.5.2 (Output Square Mesh, square_mesh.py).

def write_to_file(frameNum, x, n_seg):
    # Check if 'output' directory exists; if not, create it
    if not os.path.exists('output'):
        os.makedirs('output')

    # create obj file
    filename = f"output/{frameNum}.obj"
    with open(filename, 'w') as f:
        # write vertex coordinates
        for row in x:
            f.write(f"v {float(row[0]):.6f} {float(row[1]):.6f} 0.0\n") 
        # write vertex indices for each triangle
        for i in range(0, n_seg):
            for j in range(0, n_seg):
                #NOTE: each cell is exported as 2 triangles for rendering
                f.write(f"f {i * (n_seg+1) + j + 1} {(i+1) * (n_seg+1) + j + 1} {(i+1) * (n_seg+1) + j+1 + 1}\n")
                f.write(f"f {i * (n_seg+1) + j + 1} {(i+1) * (n_seg+1) + j+1 + 1} {i * (n_seg+1) + j+1 + 1}\n")

With all components properly set up, the next phase involves initiating the simulation loop. This loop advances the time integration and visualizes the results at each time step. To execute the simulation program, the following command is used in the terminal:

python3 simulator.py

Remark 4.5.1 (Practical Considerations). With our simulator implementation in place, it provides us with the flexibility to experiment with various configurations of the optimization time integration scheme. Such testing is invaluable for gaining deeper insights into the roles and impacts of each essential component.

Consider an example: if we opt not to project the mass-spring Hessian to a Symmetric Positive Definite (SPD) form, peculiar behaviors may emerge under certain conditions. For instance, running the simulation with a frame-rate time step size of h=0.02 and an initial_stretch of 0.5 could lead to line search failures. This, in turn, results in very small step sizes, hampering the optimization process and preventing significant progress.

While line search might seem superfluous in this simplistic 2D example, its necessity becomes apparent in more complex 3D elastodynamics simulations, especially those involving large deformations. Here, line search is crucial to ensure the convergence of the simulation.

Another point of interest is the stopping criteria applied in traditional solids simulators. Many such simulators forego a dynamic stopping criterion and instead terminate the optimization process after a predetermined number of iterations. This approach, while straightforward, can lead to numerical instabilities or 'explosions' in more challenging scenarios. This underscores the importance of carefully considering the integration scheme and its parameters to ensure stable and accurate simulations.

GPU-Accelerated Simulation

*Author of this section: Zhaofeng Luo, Carnegie Mellon University & Peking University

We now rewrite the 2D mass-spring simulator to leverage GPU acceleration. Instead of directly writing CUDA, we resort to MUDA, a lightweight library that provides a simple interface for GPU-accelerated computations.

The architecture of the GPU-accelerated simulator is similar to the Python version. All function and variable names are consistent with the Numpy version. However, the implementation details are different due to the GPU architecture and programming model. Before delving into the details, let's first get a feeling of the speedup that GPU could bring us from the following gif (Figure 4.6.1).

Figure 4.6.1. An illustration of simulation speed of the Numpy CPU (left) and the MUDA GPU (right) versions.

Key Considerations for GPU Programming

To maximize resource utilization on the GPU, there are two important aspects to consider:

  • Minimizing Data Transfer. In most modern architectures, CPU and GPU have separate memory spaces. Transferring data between these spaces can be expensive. Therefore, it is essential to minimize data transfers between CPU and GPU.
  • Exploiting Parallelism. GPUs excel at parallel computations. However, care must be taken to avoid read-write conflicts that can arise when multiple threads attempt to access the same memory locations simultaneously.

Minimizing Data Transfer

To reduce data transfer between the CPU and GPU, we store the main energy values and their derivatives on the GPU. Computations are then performed directly on the GPU, and only the necessary position information is transferred back to the CPU for control and rendering. A more efficient implementation could render directly on the GPU, eliminating even this data transfer, but for simplicity and readability, we have not implemented that here.

To make the code more readable, the variables begin with device_ are stored in the GPU memory, and the variables begin with host_ are stored in the CPU memory.

Implementation 4.6.1 (Data structure, MassSpringEnergy.cu).

template <typename T, int dim>
struct MassSpringEnergy<T, dim>::Impl
{
	DeviceBuffer<T> device_x;
	DeviceBuffer<T> device_l2, device_k;
	DeviceBuffer<int> device_e;
	int N;
	DeviceBuffer<T> device_grad;
	DeviceTripletMatrix<T, 1> device_hess;
};

As shown in the code above, the energy values and their derivatives, as well as all the necessary parameters are stored in a DeviceBuffer object, which is a wrapper of the CUDA device memory implemented by the MUDA library. This allows us to perform computations directly on the GPU without the need for data transfer between the CPU and GPU.

Newton's Method

The iterations of Newton's method is a serial process and cannot be parallelized. Therefore, we implement this part on the CPU:

Implementation 4.6.2 (Newton's method, simulator.cu).

template <typename T, int dim>
void MassSpringSimulator<T, dim>::Impl::step_forward()
{
    update_x_tilde(add_vector<T>(device_x, device_v, 1, h));
    DeviceBuffer<T> device_x_n = device_x; // Copy current positions to device_x_n
    int iter = 0;
    T E_last = IP_val();
    DeviceBuffer<T> device_p = search_direction();
    T residual = max_vector(device_p) / h;
    while (residual > tol)
    {
        std::cout << "Iteration " << iter << " residual " << residual << "E_last" << E_last << "\n";
        // Line search
        T alpha = 1;
        DeviceBuffer<T> device_x0 = device_x;
        update_x(add_vector<T>(device_x0, device_p, 1.0, alpha));
        while (IP_val() > E_last)
        {
            alpha /= 2;
            update_x(add_vector<T>(device_x0, device_p, 1.0, alpha));
        }
        std::cout << "step size = " << alpha << "\n";
        E_last = IP_val();
        device_p = search_direction();
        residual = max_vector(device_p) / h;
        iter += 1;
    }
    update_v(add_vector<T>(device_x, device_x_n, 1 / h, -1 / h));
}

In this function, step_forward, the projected Newton method with line search is implemented, performing necessary computations on the GPU while controlling the process on the CPU. Any variable begin with device_ here is a DeviceBuffer object on the GPU. To print the values in DeviceBuffer for debugging purposes, the common practice is to transfer the data back to the CPU, or call the display_vec function (which calls printf in parallel on the GPU) implemented in uti.cu.

The update_x function updates the positions of the nodes to all Energy classes and transfers the updated positions back to the CPU for rendering:

Implementation 4.6.3 (Update positions, simulator.cu).

template <typename T, int dim>
void MassSpringSimulator<T, dim>::Impl::update_x(const DeviceBuffer<T> &new_x)
{
    inertialenergy.update_x(new_x);
    massspringenergy.update_x(new_x);
    device_x = new_x;
}

As the Energy classes has already updated its positions, the IP_val function no loner needs to pass any parameters, avoiding unnecessary data transfer. In fact, it only calls the val function of all energy classes and then sum the results together:

Implementation 4.6.4 (Computing IP, simulator.cu).

template <typename T, int dim>
T MassSpringSimulator<T, dim>::Impl::IP_val()
{

    return inertialenergy.val() + massspringenergy.val() * h * h;
}

Similarly for the IP_grad and IP_hess functions:

Implementation 4.6.5 (Computing IP gradient and Hessian, simulator.cu).

template <typename T, int dim>
DeviceBuffer<T> MassSpringSimulator<T, dim>::Impl::IP_grad()
{
    return add_vector<T>(inertialenergy.grad(), massspringenergy.grad(), 1.0, h * h);
}

template <typename T, int dim>
DeviceTripletMatrix<T, 1> MassSpringSimulator<T, dim>::Impl::IP_hess()
{
    DeviceTripletMatrix<T, 1> inertial_hess = inertialenergy.hess();
    DeviceTripletMatrix<T, 1> massspring_hess = massspringenergy.hess();
    DeviceTripletMatrix<T, 1> hess = add_triplet<T>(inertial_hess, massspring_hess, 1.0, h * h);
    return hess;
}

Notice that they utilize the parallel operations (add_vector and add_triplet, which are implemented in uti.cu) on the GPU to perform the summation for gradients and Hessians.

Parallel Computations

In our implementation, parallel computation is primarily employed in the computation of energy and its derivatives, as well as vector addition and subtraction. Let's take the MassSpringEnergy computation as an example.

Energy Computation

Implementation 4.6.6 (Computing energy, MassSpringEnergy.cu).

template <typename T, int dim>
T MassSpringEnergy<T, dim>::val()
{
	auto &device_x = pimpl_->device_x;
	auto &device_e = pimpl_->device_e;
	auto &device_l2 = pimpl_->device_l2;
	auto &device_k = pimpl_->device_k;
	int N = device_e.size() / 2;
	DeviceBuffer<T> device_val(N);
	ParallelFor(256).apply(N, [device_val = device_val.viewer(), device_x = device_x.cviewer(), device_e = device_e.cviewer(), device_l2 = device_l2.cviewer(), device_k = device_k.cviewer()] __device__(int i) mutable
						   {
		int idx1= device_e(2 * i); // First node index
		int idx2 = device_e(2 * i + 1); // Second node index
		T diff = 0;
		for (int d = 0; d < dim;d++){
			T diffi = device_x(dim * idx1 + d) - device_x(dim * idx2 + d);
			diff += diffi * diffi;
		}
		device_val(i) = 0.5 * device_l2(i) * device_k(i) * (diff / device_l2(i) - 1) * (diff / device_l2(i) - 1); })
		.wait();

	return devicesum(device_val);
} // Calculate the energy

The ParallelFor function distributes the computation across multiple GPU threads. The captured variables in the lambda function allow access to the necessary data structures within each thread.

Gradient Computation

Implementation 4.6.7 (Computing gradients, MassSpringEnergy.cu).

template <typename T, int dim>
const DeviceBuffer<T> &MassSpringEnergy<T, dim>::grad()
{
	auto &device_x = pimpl_->device_x;
	auto &device_e = pimpl_->device_e;
	auto &device_l2 = pimpl_->device_l2;
	auto &device_k = pimpl_->device_k;
	auto N = pimpl_->device_e.size() / 2;
	auto &device_grad = pimpl_->device_grad;
	device_grad.fill(0);
	ParallelFor(256).apply(N, [device_x = device_x.cviewer(), device_e = device_e.cviewer(), device_l2 = device_l2.cviewer(), device_k = device_k.cviewer(), device_grad = device_grad.viewer()] __device__(int i) mutable
						   {
		int idx1= device_e(2 * i); // First node index
		int idx2 = device_e(2 * i + 1); // Second node index
		T diff = 0;
		T diffi[dim];
		for (int d = 0; d < dim;d++){
			diffi[d] = device_x(dim * idx1 + d) - device_x(dim * idx2 + d);
			diff += diffi[d] * diffi[d];
		}
		T factor = 2 * device_k(i) * (diff / device_l2(i) -1);
		for(int d=0;d<dim;d++){
		   atomicAdd(&device_grad(dim * idx1 + d), factor * diffi[d]);
		   atomicAdd(&device_grad(dim * idx2 + d), -factor * diffi[d]);	  
		} })
		.wait();
	// display_vec(device_grad);
	return device_grad;
}

The atomicAdd function is crucial in the gradient computation to ensure safe concurrent updates to shared data (different edges can update the gradient of the same node), thus preventing race conditions.

Hessian Computation

We utilized the Sparse Matrix data structure to store the Hessian matrix. The computation is parallelized across multiple threads, with each thread updating a specific element of the Hessian matrix. The actual size of the Sparse Matrix is calculated at the start of the simulation, allocating just enough memory for non-zero entries. The main consideration here is to calculate the correct indices for each element during simulation:

Implementation 4.6.8 (Computing Hessians, MassSpringEnergy.cu).

template <typename T, int dim>
const DeviceTripletMatrix<T, 1> &MassSpringEnergy<T, dim>::hess()
{
	auto &device_x = pimpl_->device_x;
	auto &device_e = pimpl_->device_e;
	auto &device_l2 = pimpl_->device_l2;
	auto &device_k = pimpl_->device_k;
	auto N = device_e.size() / 2;
	auto &device_hess = pimpl_->device_hess;
	auto device_hess_row_idx = device_hess.row_indices();
	auto device_hess_col_idx = device_hess.col_indices();
	auto device_hess_val = device_hess.values();
	device_hess_val.fill(0);
	ParallelFor(256).apply(N, [device_x = device_x.cviewer(), device_e = device_e.cviewer(), device_l2 = device_l2.cviewer(), device_k = device_k.cviewer(), device_hess_val = device_hess_val.viewer(), device_hess_row_idx = device_hess_row_idx.viewer(), device_hess_col_idx = device_hess_col_idx.viewer(), N] __device__(int i) mutable
						   {
		int idx[2] = {device_e(2 * i), device_e(2 * i + 1)}; // First node index
		T diff = 0;
		T diffi[dim];
		for (int d = 0; d < dim; d++)
		{
			diffi[d] = device_x(dim * idx[0] + d) - device_x(dim * idx[1] + d);
			diff += diffi[d] * diffi[d];
		}
		Eigen::Matrix<T, dim, 1> diff_vec(diffi);
		Eigen::Matrix<T, dim, dim> diff_outer = diff_vec * diff_vec.transpose();
		T scalar = 2 * device_k(i) / device_l2(i);
		Eigen::Matrix<T, dim, dim> H_diff = scalar * (2 * diff_outer + (diff_vec.dot(diff_vec) - device_l2(i)) * Eigen::Matrix<T, dim, dim>::Identity());
		Eigen::Matrix<T, dim * 2, dim * 2> H_block, H_local;
		H_block << H_diff, -H_diff,
			-H_diff, H_diff;
		make_PSD(H_block, H_local);
		// add to global matrix
		for (int ni = 0; ni < 2; ni++)
			for (int nj = 0; nj < 2; nj++)
			{
				int indStart = i * 4*dim*dim + (ni * 2 + nj) * dim*dim;
				for (int d1 = 0; d1 < dim; d1++)
					for (int d2 = 0; d2 < dim; d2++){
						device_hess_row_idx(indStart + d1 * dim + d2)= idx[ni] * dim + d1;
						device_hess_col_idx(indStart + d1 * dim + d2)= idx[nj] * dim + d2;
						device_hess_val(indStart + d1 * dim + d2) = H_local(ni * dim + d1, nj * dim + d2);
					}
			} })
		.wait();
	return device_hess;
} // Calculate the Hessian of the energy

Summary

We have successfully demonstrated the implementation of a basic 2D mass-spring simulator encompassing several critical components:

  • Mesh Generation: This involves the creation of nodes and connecting elements. In practical scenarios, simulators often import meshes from pre-existing files.
  • Incremental Potential Energy Evaluation: Comprises the computation of the potential energy value, its gradient, and the Symmetric Positive Definite (SPD)-projected Hessian.
  • Optimization Time Integrator: This includes linear solves for determining search directions, line search techniques to ensure global convergence, and rules for updating nodal positions and velocities.
  • Simulator Structure: Encompasses scene setup, variable initialization, and the execution of the simulation loop. (Note: Visualization aspects can be decoupled from the simulator itself.)

In the forthcoming chapter, we will delve into boundary treatments, including prescribed motion and frictional contact, which are implemented through equality or inequality constraints in the optimization framework. This discussion will be enriched with practical case studies, illustrating the application of each boundary treatment in computational simulations.

Dirichlet Boundary Conditions*

Boundary treatments, including boundary conditions and frictional contacts, play a crucial role in solid simulations. They not only enhance the expressiveness of scene setup but also capture intricate dynamics within the simulation. This lecture introduces Dirichlet boundary conditions, a pivotal concept for prescribing the motion of specific nodes in solid structures. Understanding these conditions is essential for accurately modeling and manipulating the behavior of solids in various simulation scenarios.

Equality Constraint Formulation

Dirichlet boundary conditions (BC), when integrated into the optimization time integrator, are represented as linear equality constraints: In this equation, the matrix \(A\) is a \(m \times dn\) matrix, where \(m \leq dn\). This matrix functions to select the degrees of freedom (DOFs) at the nodes that are subject to the boundary conditions. The vector \(b\) is a \(m \times 1\) vector, which specifies the precise spatial values that are prescribed by these conditions.

Example 5.1.1 (Sticky Dirichlet Boundary Condition). For a 2D system containing two nodes \((x_{11}, x_{12})\) and \((x_{21}, x_{22})\), to fix the second node at position \((1, 2)\), the boundary condition (Equation (5.1.1)) can be expressed as

The two most common types of Dirichlet boundary conditions are sticky and slip:

Sticky Boundary Conditions: These conditions effectively fix the position of certain nodes within a time step. They are characterized by a block-wise constraint Jacobian matrix \(A\). In this matrix, each set of \(d\) rows includes exactly one \(d \times d\) identity matrix. The rest of the matrix consists of zero matrices. This configuration is illustrated in Example 5.1.1. The implementation of sticky boundary conditions ensures that the specified nodes remain stationary, adhering to the prescribed positions during the simulation.

Slip Boundary Conditions: These conditions are designed to constrain each boundary condition (BC) node within a specific linear subspace, such as a plane or a line, which may not necessarily be axis-aligned. As an example, consider planar slip boundary conditions. Here, for each BC node, there is a corresponding row in the matrix \(A\) that contains the normal vector of the plane. This vector occupies the columns corresponding to the BC node, as detailed in Example 5.1.2. Such conditions allow the nodes to move, but only within the defined linear subspace, thus adding a layer of complexity and realism to the simulation.

Example 5.1.2 (Slip Dirichlet Boundary Condition). For the same two-node system in Example 5.1.1, to constrain the first node in the line with equation \(2x + 3y = 4\), the constraint (Equation (5.1.1)) can be expressed as

At the start of each time step, if we are given that all boundary conditions are satisfied, then the goal during optimization is simply to maintain the positions of the boundary condition nodes. This is represented as: Here, \(\Delta x\) is the search direction in each optimization iteration. Maintaining this condition ensures that any updated nodal position \(x + \alpha \Delta x\), with \(\alpha\) being the step size from line search, still satisfies the boundary conditions: This guarantees the adherence to boundary conditions throughout the optimization process.

To enforce the linear equality constraints (Equation (5.1.2)) for sticky DBC in a time step, we address this in each Newton iteration while solving for the search direction \( \Delta x \). This process involves forming the Lagrangian with a quadratic approximation to the Incremental Potential:

Here, \( \lambda \) is the \( m\times 1 \) Lagrange multiplier vector. The gradient and Hessian of the Incremental Potential are denoted by \( g \) and \( H \), respectively.

The solution is approached through a max-min optimization problem:

which leads to the formulation of a Karush-Kuhn-Tucker (KKT) system:

Solving this KKT system is essential to determine the search direction. Note that this system is not Symmetric Positive Definite (SPD) and its size increases with the number of BC nodes.

DOF Elimination Method

Considering the simplest sticky Dirichlet boundary condition as an example, its constraint Jacobian \( A \) acts as a selection matrix. Consequently, \( AA^T \) forms a \( m \times m \) identity matrix, and \( A^T A \) becomes a \( dn \times dn \) diagonal matrix. In this matrix, the entries corresponding to the BC nodes are one, and all other entries are zero.

When we left-multiply \( A \) to the first block row of Equation (5.1.3), the resulting equation is:

This manipulation allows us to directly solve for \( \lambda \) as:

By substituting Equation (5.2.1) back into the first block row of Equation (5.1.3), we derive the following equation:

Here, left-multiplying by \((I - A^T A)\) effectively zeroes out the rows corresponding to the BC nodes. Hence, Equation (5.2.2) represents an under-constrained system. However, the second block row of Equation (5.1.3) actually provides us with the values of \(\Delta x\) at the BC nodes (so they are not really unknowns). By considering this information, we can rewrite Equation (5.2.2) into a Symmetric Positive Definite (SPD) system:

where the matrices and vectors are partitioned as follows:

and the subscript \(B\) denotes the BC nodes. Knowing that \(\Delta x_B = 0\), the system simplifies to:

which represents a SPD system that excludes the BC nodes.

A More Practical Approach

The method outlined above serves primarily for mathematical explanation. In practical applications, constructing Equation (5.2.3) is often avoided. This is because it entails reordering degrees of freedom (DOFs) and separating the BC nodes from unconstrained nodes, a process that can be both tedious and inefficient, particularly when the set of Dirichlet nodes varies over time.

To circumvent the need to reorder DOFs, a direct modification of the original linear system can be made to align it with Equation (5.2.3). This adjustment involves setting all entries in the rows corresponding to BC nodes in \( H \) and \( g \) to \( 0 \). Additionally, for the columns associated with BC nodes in \( H \), all off-diagonal entries are set to \( 0 \) while diagonal entries are assigned \( 1 \) or another positive real number to ensure the system remains well-conditioned. After solving this modified system, the resulting values of \( \Delta x_U \) are immediately aquired, and all \( \Delta x_B \) values are guaranteed to be \( 0 \).

Example 5.2.1 (DOF Elimination). For the problem defined in Example 5.1.1 where the second node \((x_{21}, x_{22})\) is fixed at \((1,2)\) in a 2D two-node system, assuming in a certain iteration of a time step we solve the system for search direction \(\Delta x\) so that \(\Delta x_{21} = \Delta x_{22} = 0\) and after line search we for sure know that \((x_{21}, x_{22}) = (1, 2)\) still holds since . Here (5.2.4) is essentially

Remark 5.2.1 (Limitations of DOF Elimination). The DOF elimination method described is effective when sticky BC nodes are established at the beginning of the time step. However, if this is not the case, and the constraint function in Equation (5.1.3) has a non-zero right-hand side (rhs), the DOF elimination method becomes inapplicable. The issue here is not the inability to solve for \( \Delta x \) under constraints with a non-zero rhs. Rather, the concern is that the resulting \( \Delta x \) might not lead to a descent direction in the Incremental Potential. This can result in exceedingly small step sizes after a line search, potentially stalling the optimization process.

Intuitively, if the direction of \( \Delta x_B \) is towards the prescribed BC coordinates, it could inadvertently increase the Incremental Potential, which is not adjusted to consider the BCs. Conversely, if \( \Delta x_B \) is simply \( 0 \) when the BCs are already satisfied, it effectively minimizes the Incremental Potential using a subset of variables, which remains a valid approach.

One might then ask why not adjust the DOFs to meet the BCs before starting the optimization. However, this strategy could lead to infeasible configurations, such as those involving intersections. A viable alternative is to initially apply stiff spring forces to gradually 'drag' the BC nodes to their constrained positions during optimization. After this, switching to the DOF elimination method can enhance convergence. This technique is further discussed in the section Moving Boundary Conditions*.

Case Study: Hanging Square*

We use a simple case study to end this lecture. Based on the mass-spring system developed in a previous section, we implement gravitational energy and sticky Dirichlet boundary conditions to simulate a hanging square. The excutable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial under the 2_dirichlet folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/2_dirichlet folder.

Gravitational energy has which can be trivially implemented:

Implementation 5.3.1 (GravityEnergy.py).

import numpy as np

gravity = [0.0, -9.81]

def val(x, m):
    sum = 0.0
    for i in range(0, len(x)):
        sum += -m[i] * x[i].dot(gravity)
    return sum

def grad(x, m):
    g = np.array([gravity] * len(x))
    for i in range(0, len(x)):
        g[i] *= -m[i]
    return g

# Hessian is 0

Then we just need to make sure the gravitational energy is added into the Incremental Potential (IP):

Implementation 5.3.2 (Adding gravity to IP, time_integrator.py).

def IP_val(x, e, x_tilde, m, l2, k, h):
    return InertiaEnergy.val(x, x_tilde, m) + h * h * (MassSpringEnergy.val(x, e, l2, k) + GravityEnergy.val(x, m))     # implicit Euler

def IP_grad(x, e, x_tilde, m, l2, k, h):
    return InertiaEnergy.grad(x, x_tilde, m) + h * h * (MassSpringEnergy.grad(x, e, l2, k) + GravityEnergy.grad(x, m))   # implicit Euler

For the sticky Dirichlet boundary condition, we modify the system accordingly when computing search direction:

Implementation 5.3.3 (DOF elimination, time_integrator.py).

def search_dir(x, e, x_tilde, m, l2, k, is_DBC, h):
    projected_hess = IP_hess(x, e, x_tilde, m, l2, k, h)
    reshaped_grad = IP_grad(x, e, x_tilde, m, l2, k, h).reshape(len(x) * 2, 1)
    # eliminate DOF by modifying gradient and Hessian for DBC:
    for i, j in zip(*projected_hess.nonzero()):
        if is_DBC[int(i / 2)] | is_DBC[int(j / 2)]: 
            projected_hess[i, j] = (i == j)
    for i in range(0, len(x)):
        if is_DBC[i]:
            reshaped_grad[i * 2] = reshaped_grad[i * 2 + 1] = 0.0
    return spsolve(projected_hess, -reshaped_grad).reshape(len(x), 2)

Here is_DBC is an array marking whether a node is Dirichlet or not as we store the Dirichlet node indices in DBC:

Implementation 5.3.4 (DBC definition, simulator.py).

DBC = [n_seg, (n_seg + 1) * (n_seg + 1) - 1]  # fix the left and right top nodes

# ...

# identify whether a node is Dirichlet
is_DBC = [False] * len(x)
for i in DBC:
    is_DBC[i] = True

Finally, after making sure is_DBC is passed to the time integrator, we can simulate an energetic hanging square (no initial stretching) with a smaller spring stiffness k=1e3 at framerate time step size h=0.02 (Figure 5.3.1).

Figure 5.3.1. From left to right: initial, intermediate, and final static frame of the hanging square simulation.

Summary

In this section, we explored Dirichlet boundary conditions (DBC), integral to optimization time integrators, and presented them as straightforward linear equality constraints. There are two types of DBCs: sticky and slip. Sticky DBCs immobilize certain nodes, fixing their positions, whereas slip DBCs restrict the movement of nodes to within a plane or a line.

We focused on cases where sticky DBCs are already met at the start of a time step. In such scenarios, the DOF elimination method proves efficient. This technique modifies the gradient and Hessian of the Incremental Potential, ensuring that the resulting search direction remains within the feasible space.

In the following lecture, we will delve into the handling of slip DBCs and demonstrate methods for their efficient incorporation into optimization problems.

Slip Dirichlet Boundary Conditions

Although they might be satisfied at the start of a time step, general slip Dirichlet boundary conditions (DBC) present unique challenges. Unlike the sticky DBCs, they cannot be directly addressed using the DOF elimination method, primarily because their constraint Jacobian does not consist of identity matrix blocks. To navigate this complexity, we can adopt a change-of-basis strategy.

Before delving into the more general scenarios, it's insightful to first examine a particular type of slip DBC: those that are axis-aligned. Understanding this specific case will lay the groundwork for tackling the broader range of slip DBCs.

Axis-Aligned Slip DBC

Axis-Aligned slip Dirichlet boundary conditions (DBC) uniquely restrict the movement of certain nodes to linear subspaces that are aligned with the axes. For instance, these constraints could limit motion to lines parallel to the x-axis or planes parallel to the yz-plane. An advantageous aspect of Axis-Aligned slip DBC is that their constraint Jacobians bear resemblance to those of sticky DBCs. Consequently, they can be efficiently managed using the same DOF elimination method.

Example 6.1.1 (Axis-Aligned Slip DBC). Consider the previously mentioned two-node system in a 2D space, as referenced in the slip DBC example (Example 5.1.2). To apply a slip DBC that constrains the first node, represented by coordinates \((x_{11}, x_{12})\), to move only along the line \(y = 3\), we express this constraint as a linear equality: Then similar to sticky DBC, in a time step where this slip DBC is already satisfied, assume we have we can solve the system for search direction so that \(\Delta x_{12} = 0\) and the first node will stay on the \(y=3\) line for arbitrary step size since its \(y\) coordinate will not vary.

Change of Variables

Challenges with General Slip DBCs and the DOF Elimination Method

When dealing with general linear equality constraints, such as slip DBCs that aren't axis-aligned, the direct Degree of Freedom (DOF) elimination method faces certain limitations. This becomes evident particularly when \( AA^T \) is not an \( m \times m \) identity matrix. According to the Karush-Kuhn-Tucker (KKT) system (Equation (5.1.3)), the Lagrange multiplier vector \( \lambda \) can be solved as follows:

When we substitute Equation (6.2.1) back into the KKT system, it results in:

This leads to an under-constrained system. The key challenge here is that \( I - A^T (AA^T)^{-1} A \) does not possess a special structure that can be conveniently exploited to derive an equivalent, non-singular system while still satisfying the constraints. This makes the direct application of the DOF elimination method impractical for general slip DBCs.

Simplifying Constraints Using Singular Value Decomposition

Our approach involves transforming the degrees of freedom (DOF) into a new set of variables, making the constraints as straightforward as those in sticky DBC. To achieve this, we employ singular value decomposition (SVD) on the constraint Jacobian matrix \( A \). The SVD of \( A \) is expressed as: Here, \( U \) is a \( m \times m \) orthogonal matrix, \( V \) is a \( dn \times dn \) orthogonal matrix, and \( S \) is a \( m \times dn \) diagonal matrix.

By defining \( y = V^T \Delta x \), we can reframe the Karush-Kuhn-Tucker (KKT) system (Equation (5.1.3)) into a new format:

In this transformed system, \( \lambda' = U^T \lambda \). Notably, the presence of the diagonal matrix \( S \) in the off-diagonal blocks allows the direct application of the DOF elimination method. Once we solve for \( y \), the original variable \( \Delta x \) is easily recovered through the matrix-vector product \( \Delta x = V y \).

Remark 6.2.1 (Limitations of Using SVD for DOF Elimination). While we utilized singular value decomposition (SVD) to illustrate the concept, it's important to recognize the limitations of applying SVD in practice, especially on large matrices. There are two primary concerns:

  1. Intractability with Large Matrices: Performing SVD on matrices of substantial size can be computationally challenging and often impractical.
  2. Impact on Computational Efficiency: The Incremental Potential Hessian \( H \) typically exhibits sparsity, making it efficient to factorize in linear solves during simulations. However, if the resulting \( V \) from the SVD is dense, then \( V^T H V \) will also be dense. This not only slows down the computation but also significantly increases the cost of linear solves.

It's crucial to note that the new basis set (the column vectors of \( V \)) needs to be linearly independent but does not necessarily have to be orthonormal. This insight opens up the possibility of identifying a sparse basis set. Such a set can maintain computational efficiency when dealing with general linear equality constraints. For a practical example of this approach, see [Chen et al. 2022].

General Slip DBC

Fortunately, for constraints like slip DBCs that are decoupled per node, SVD simply results in block-diagonal and which could be constructed procedurally in an efficient way. 3D planar slip DBC at node can be expressed as where is the normal of the plane that node is slipping, and is an arbitrary point on that plane. As discussed near Equation (5.1.2), if at the beginning of the time step node is already on the plane, the constraint simplifies to Then performing SVD on the row vector , we obtain where unit vectors , , and together form an orthonormal basis in 3D.

Then it becomes clear that globally, is simply a identity matrix, is a matrix where every row contains exactly one unit-valued entry in the column corresponding to the first DOF of the slip BC node, and is a block-diagonal matrix with the orthonormal blocks only on those corresponding to BC nodes, and identity matrix elsewhere.

To compute and from , we first note that there are an infinite number of possible solutions. Therefore, we can simply first construct , or if is almost colinear with , and then construct . To obtain , one only needs to left-multiply each to . As for , first left-multiply each to every block on the -th block row of to obtain . Then for the -th block column of , left-multiply to every block. Finally, after solving for by applying the DOF elimination method on the modified system (Equation (6.2.3)), can be obtained by with similar block(node)-wise operations.

Example 6.3.1 (General Slip DBC). For the same two-node system in 2D as mentioned in the slip DBC example (Example 5.1.2), to constrain the first node inside the line, the slip DBC can be expressed as and we can build for changing the basis. Then in a time step where this slip DBC is already satisfied, assume we have we can compute and solve the system for . Then the search direction can be obtained by so that and so the first node will stay on the line for arbitrary step size.

Summary

This section has demonstrated that, with a change in the basis of variables, general slip Dirichlet boundary conditions (DBC) can be effectively managed using the Degree of Freedom (DOF) elimination method, much like axis-aligned slip DBCs.

While singular value decomposition (SVD) can be used to find the basis for general linear equality constraints, this approach may not be feasible for large or complex constraints. Nonetheless, it's possible to develop procedural routines for computing the basis, specifically tailored to node-wise slip DBC constraints.

Currently, our focus has been on maintaining DBCs that are already satisfied within the simulation framework. Moving forward, the discussion will shift towards exploring frictional contact between points and analytic surfaces. Additionally, we will revisit scenarios where DBCs are not satisfied at the start of a time step, delving into more complex cases.

Distance Barrier for Nonpenetration

Contact modeling is a crucial aspect of ensuring that solids do not intersect with obstacles or themselves. This topic was briefly touched upon in a previous section. In this lecture, we delve deeper into the specifics of non-interpenetration within the framework of the Incremental Potential Contact (IPC) method. Our focus will be on a simplified yet significant scenario: contact between solids and obstacles that have closed boundaries. This specific focus allows us to thoroughly explore the mechanics and principles of the IPC method in a controlled setting.

Signed Distances

The Incremental Potential Contact (IPC) method is designed to ensure non-interpenetration in solids of any codimension by maintaining the unsigned distances between solid boundaries above zero throughout their movement. This approach is robust as it applies universally, irrespective of the solid's specific characteristics.

However, when signed distances are accessible, the application of IPC becomes not only straightforward but also more streamlined. Signed distances extend the concept of unsigned distances to encompass solid geometries with closed boundaries. With IPC enforcing non-interpenetration, the possibility of negative distances inside a solid is eliminated. Therefore, in scenarios where signed distances remain non-negative (including the state of being exactly zero), it's an indication of successful non-interpenetration.

Definition 7.1.1 (Codimension). If \(W\) is a linear subspace of a finite-dimensional vector space \(V\), then the codimension of \(W\) in \(V\) is the difference between their dimensions: For example, in 3D, a surface has codimension \(1\), and a line has codimension \(2\). In computer graphics, when simulating cloth and hair, codimension 1 and 2 geometry representations are often applied respectively for efficiency. However, their signed distances are not well-defined. This also explains why unsigned distances are more general for modeling solid contact.

In a previous section, we explored various methods for representing solid geometries. One notable approach is the analytical representation. For instance, a 3D ball centered at \( \mathbf{c} \) with radius \( r \) can be analytically described by the parameterization:

This principle of defining solid geometries extends beyond simple spheres. Many other shapes, such as half-spaces, boxes, ellipsoids, and tori, can be similarly parameterized. The key to these parameterizations lies in defining the "interior" of these objects, which can often be achieved through functions like signed distances. These functions provide a versatile tool for describing a wide range of simple and complex shapes in a concise and mathematical manner.

Example 7.1.1 (Ball Signed Distance Function). The signed distance function \(d(\mathbf{x})\) and its derivatives of a ball centered at \(\mathbf{c}\) with radius \(r\) can be defined as

Example 7.1.2 (Half-Space Signed Distance Function). The signed distance function \(d(\mathbf{x})\) and its derivatives of a half-space with normal \(\mathbf{n}\) and \(d(\mathbf{o}) = 0\) can be defined as

Representing more intricate geometries, like those commonly encountered in real-life scenarios, can be a challenging task due to their complexity. An effective alternative to intricate parameterizations is the use of a uniform Euclidean grid. This grid serves as a storage mechanism for the signed distances of a solid object, with these distances precomputed at each grid node. When the distance at any arbitrary point within the solid is required, interpolation can be applied to the grid data.

Example 7.1.3 (Grid Signed Distance Field). For a signed distance field stored on a uniform Euclidean grid with spacing \(\Delta x\), to query the distance at an arbitrary location \(\mathbf{x} = (x,y)\) where \(x = x_i + \alpha \Delta x\) and \(y = y_i + \beta \Delta x\) (\(\mathbf{x}_{i,j} = (x_i, y_j)\) are the location of grid nodes, \(0 \leq \alpha,\beta \leq 1\)), with bilinear interpolation (Figure 7.1.1 right), From Figure 7.1.1 we also see that to approximate a solid boundary smoothly in this setting, a higher-order interpolation scheme such as quadratic b-spline interpolation is needed.

Figure 7.1.1. The signed distance between the grid nodes and the sphere boundary is precomputed and stored (left). With bilinear interpolation, part of the sphere boundary is approximated as the blue polyline (right).

Distance Barrier

Constrained Optimization

In scenarios like a solid interacting with a planar ground, where the signed distance function \( d(\mathbf{x}) \) is smooth outside the obstacle, we can approach the modeling of contact by incorporating non-interpenetration constraints. These constraints are formulated using \( d(\mathbf{x}) \), while we also aim to minimize the Incremental Potential of the system.

Assuming that the solids are densely sampled with nodes \(\mathbf{x}\), we apply these constraints at the level of nodal Degrees of Freedom (DOFs) in relation to the obstacles:

In this equation, \( d_{ij} \) represents the signed distance between node \( i \) and obstacle \( j \). By ensuring that \( d_{ij} \) is non-negative, we effectively prevent the solids from intersecting with the obstacles1.

Logarithm Barrier Potential in Contact Modeling

To address the inequality constraints in our contact modeling, we introduce a barrier potential \( P_b(\mathbf{x}) \). This potential transforms the constrained problem, as described in Equation (7.2.1), into an "unconstrained" optimization problem:

The barrier potential is defined as follows:

In this formulation, \( b() \) represents the barrier energy density function. As the distance approaches zero, this function tends to infinity, thereby providing a strong repulsion force to prevent interpenetration (refer to Figure 7.2.1). The distance threshold \( \hat{d} \) above which no contact force is exerted, the contact stiffness \( \kappa \) which controls the rate of change of the contact forces with respect to distance, and \( A_i \), the contact area of node \( i \), are key parameters in this setup. By integrating the energy density over the solid boundary, the barrier formulation effectively models a potential energy field that is of thickness \( \hat{d} \).

Figure 7.2.1. The barrier energy density function plotted with different . Decreasing asymptotically matches the discontinuous definition of the contact condition.

Remark 7.2.1 (Contact Layer Interpretation). Imagine the barrier potential \( P_b(\mathbf{x}) \) as representing the elasticity of an ultra-thin layer of virtual material that exists just outside the boundaries of the solids. This virtual layer has an effective thickness of \( \hat{d} \), which correlates with the distance threshold in the barrier function.

Consequently, the integration or summation used in computing \( P_b(\mathbf{x}) \) is weighted by the volume element \( w_i = A_i \hat{d} \), where \( A_i \) represents the contact area of each node. As solids approach and begin to compress this virtual elastic layer, contact forces arise. These forces, akin to a unique type of elasticity force, serve to prevent interpenetration by providing a repulsion effect whenever the solids come too close to each other. This model allows us to simulate the physical response of contact without actual penetration of the solids.

Applying chain rules with distance being the intermediate variables, we can derive the gradient and Hessian of \(P_b(\mathbf{x})\) as and

1

As we are using signed distances here, the inequality constraints can be defined without introducing an \(\epsilon\) as in Equation (2.3.1) with unsigned distances.

Solution Accuracy

So why can we solve Equation (7.2.2) to approximate the solution of the original problem in Equation (7.2.1)? Similar to Dirichlet Boundary Conditions, at the solution \(x^*\) of Equation (7.2.1), the following KKT conditions all hold: While at the local optimum \(x'\) of Equation (7.2.2), we have which is equivalently and if we plug in the expression of \(\nabla b(d_{ij})\). Let \(\gamma_{ij}' = -h^2 A_i \hat{d} \frac{\partial b}{\partial d}(d_{ij}(x'))\), we can further rewrite Equation (7.3.2) as which is essentially the stationarity condition (first line in Equation (7.3.1)) if we take \(\gamma_{ij}'\) as the dual variable. Now since the barrier function provides arbitrarily large repulsion to avoid interpenetration, we know that \(\forall i,j\), \(d_{ij}(x') \geq 0\). In addition, \(\gamma_{ij}' \geq 0\) also holds for all \(i,j\) because \(\frac{\partial b}{\partial d} \leq 0\) by construction. This means that at \(x'\), we have momentum balance, no interpenetrations, and contact forces only push but not pull.

In our simulation, the only Karush-Kuhn-Tucker (KKT) condition not strictly satisfied at \( x' \) is the complementarity slackness condition. This arises due to the way our barrier approximation functions. Specifically, we have a situation where \( \gamma_{ij} > 0 \Longleftrightarrow 0 < d_{ij} < \hat{d} \), representing the activation of contact forces based on the distance between solids and obstacles.

As the threshold \( \hat{d} \) decreases, contact forces become active only when the solids are in closer proximity (as illustrated in Figure 7.2.1). This adjustment leads to a reduction in the complementarity slackness error, which can be controlled to a certain extent. However, it's important to note that this control comes at a cost: computational efficiency may be reduced. This is because sharper objective functions, resulting from smaller \( \hat{d} \) values, tend to require more Newton iterations to resolve. Therefore, there is a trade-off between the accuracy of the simulation (in terms of adhering to the KKT condition) and the computational resources required.

Summary

In simulating contact between solids and obstacles, we primarily focus on enforcing non-negativity on the signed distances between solid degrees of freedom (DOFs) and obstacles, in conjunction with minimizing the Incremental Potential.

  • Transformation to an Unconstrained Problem: The inherent inequality-constrained minimization issue for each time step is transformed into an unconstrained problem. This is achieved through the introduction of a barrier potential. This potential rises to infinity as distances approach zero, effectively generating large repulsion forces that prevent interpenetration.

  • Outcomes at Local Minimum: At the local minimum of this barrier-augmented Incremental Potential, we attain a balance of momentum, ensure non-interpenetration, and generate contact forces that only push but do not pull. The only exception in the Karush-Kuhn-Tucker (KKT) conditions is the complementarity slackness, which is not strictly satisfied. The accuracy in satisfying this condition can be controlled by adjusting the distance threshold , albeit at the expense of computational efficiency.

  • Limitations and Next Steps: While the distance barrier method effectively addresses many contact scenarios, it cannot alone prevent artificial tunneling in dynamic simulations. To overcome this limitation, our next lecture will introduce the filtered line search scheme, an advanced technique designed to provide more guarantees to our simulations.

Remark 7.4.1 (Tunneling). Artificial tunneling in the context of simulations, particularly in computational physics and computer graphics, refers to a phenomenon where fast-moving objects pass through other objects or barriers without physically interacting with them, as if there were a tunnel through the barrier. This typically happens in scenarios involving discrete time steps, such as in computer simulations of physical systems.

In a real-world scenario, when two objects collide, there should be a physical interaction like a bounce, a stop, or a deformation. However, in a simulation with discrete time steps, if an object is moving very fast or the time steps are too large, the object's position might be calculated as being on one side of a barrier in one step and then on the other side in the next, without ever detecting a collision. This "skipping" of the collision step leads to what appears as tunneling through the object.

Filter Line Search*

The Incremental Potential Contact (IPC) method effectively maintains non-interpenetration constraints within solid simulations. This method models a constitutive relationship that directly correlates contact forces with their respective distances, thus converting the constrained problem into an unconstrained one. By using appropriately small time steps, the IPC allows for robust and accurate solid simulations free from obstacle interpenetration within an optimization-based time integration framework.

However, challenges arise when using larger time steps, which can introduce multiple local minima in the Incremental Potential. This condition can lead to tunneling issues, where solids might unexpectedly pass through obstacles due to overly large search directions. To mitigate this risk, we introduce a filter line search strategy supplemented by continuous collision detection (CCD). This approach is designed to prevent tunneling by continuously adjusting the trajectory of solids in response to potential collisions.

To illustrate these concepts, we will examine a case study where an elastic square falls onto the ground. This example will demonstrate the effectiveness of the IPC method along with the filter line search and CCD in managing the dynamics of solid bodies and ensuring accurate, interpenetration-free simulations.

The Tunneling Issue

Example 8.1.1 (Tunneling). Let's consider a simple illustrative example. Without external forces like gravity, for a particle (no elasticity) at \(\mathbf{x}_0 = (0, 0)\) with mass \(m\) and initial velocity \(\mathbf{v}_0 = (1, 0)\) hitting a fixed square obstacle centered at \((0.005, 0) \), the Incremental Potential minimization problem for the first time step is Since \(\hat{d}\) is usually set small enough such as \(10^{-4}m\) in this case, the barrier potential \(P_b(\mathbf{x})\) is not yet active at \(\mathbf{x}_0\) as the particle is not touching the obstacle. This makes the problem in Equation (8.1.1) quadratic, and our projected Newton (PN) method (Algorithm 3.3.1) will produce a search direction at the first iteration, which directly leads to the global minimum of the Incremental Potential at \(\mathbf{x}_0 + h\mathbf{v}_0\) after line search. Taking \(h=0.01s\) (Figure 8.1.1), the particle will tunnel through the obstacle. However, scenarios where particles pass through obstacles due to large time steps are clearly unrealistic, as the expected physical behavior is for the particle to collide with the obstacle and either stop or bounce back.

Figure 8.1.1. An illustration of the tunneling issue. With the projected Newton method introduced earlier, tunneling artifact could happen as shown in the middle. The physically plausible result shown on the right could be obtained with the filter line search scheme. The blue arrows show the optimization path.

From Example 8.1.1, we understand that simply ensuring the signed distances to be non-negative at the final solution is inadequate, especially in scenarios involving large time step sizes, high-speed impacts, or thin obstacles. These conditions can lead to inaccuracies and unrealistic outcomes in simulations.

The Incremental Potential Contact (IPC) method addresses this issue by ensuring that distances remain non-zero across the entire motion trajectory of solids. This approach is crucial for maintaining the physical accuracy and realism of the simulation.

But what exactly do we mean by "motion trajectory" in the context of discrete time integration? We will explain this next.

Penetration-free Trajectory

The most straightforward way of defining the motion trajectory between \(x^n\) and \(x^{n+1}\) at time \(t^n\) and \(t^{n+1}\) respectively would be the high-dimensional line segment connecting these two configurations. However, although enforcing non-negative signed distances on this trajectory could avoid the tunneling issue in Example 8.1.1, this strategy could potentially result in unrealistic behaviors as it alters the local optimum of the minimization problem (Equation (7.2.1)) in a nonphysical way (Figure 8.2.1).

Figure 8.2.1. For the setup in the tunneling example, enforcing non-negative signed distance along the motion trajectory approximated by the line segment between and results in a nonphysical simulation result.

A more rigorous definition of the motion trajectory between \(x^n\) and \(x^{n+1}\) could be However, evaluating the configurations on this trajectory requires solving extra optimization problems, which could significantly complicate the time integration.

Instead, IPC takes the optimization path as an approximation to the motion trajectory. Specifically, for the time step solving from \(x^n\) to \(x^{n+1}\), if the optimization took \(l\) iterations, and each iteration we get iterate \(x^i\) after line search, the optimization path is simply the high-dimensional polyline Now the time integration problem in time step \(n\) becomes finding such optimization path \(x^0, x^1, ..., x^l\) where \(x^l\) locally minimizes the Incremental Potential (Equation (7.2.2)) subject to This enables enforcing the non-negative distance constraints per optimization iteration on the line segment between \(x^i\) and \(x^{i+1}\), which will not alter the local optimum of the time integration problem, and can be handled efficiently.

Recall from Algorithm 3.2.1 that the line search scheme updates the iterate as \(x^{i+1} \leftarrow x^i + \alpha p\), which means \(x^{i+1} - x^{i} = \alpha p\). Therefore, given an interpenetration-free \(x^i\), to ensure all the configurations on the line segment between \(x^i\) and \(x^{i+1}\) are interpenetration-free, we just need to find such \(\alpha\) that makes sure Based on the intuition that a sufficiently small \(\alpha\) could definitely make this happen, we can simply calculate an upper bound of such \(\alpha\) in every iteration, and make sure the backtracking line search results in a step size smaller than this upper bound. This upper bound can be conveniently calculated with continuous collision detection (CCD).

Definition 8.2.1 (Continuous Collision Detection (CCD)). For a distance function \(d_{jk}(x + \alpha p)\) defined with the initial interpenetration-free configuration of the solids and obstacles \(x\), their intended displacement \(p\), and the step size \(\alpha\), CCD calculates the step size \(\alpha^C_{jk}\) given \(x\) and \(p\) such that Note that the problem definition implicitly requires \(d_{jk}(x) > 0\). Under this setting, if we denote \(d^a_{jk}(\alpha) = d_{jk}(x + \alpha p)\), \(\alpha^C_{jk}\) is simply the smallest positive real root of \(d^a_{jk}(\alpha)\) (see Figure 8.2.2 for an example), or \(\alpha^C_{jk} = \infty\) if \(d^a_{jk}(\alpha)\) does not have any positive real roots. There are many methods to obtain the exact or a conservative estimate of \(\alpha^C_{jk}\), we will see a specific example in the case study of this lecture. After computing \(\alpha^C_{jk}\) for all nodes \(j\) and obstacle \(k\), a step size upper bound \(\alpha^C\) for the line search could then be obtained as

Figure 8.2.2. An illustration of CCD with a solid particle at hitting a fixed vertical plane at . With the intended displacement , we obtain .

Now, we can introduce our filter line search method (Algorithm 8.2.1), specifically designed to enforce non-interpenetration constraints throughout the entire approximated motion trajectory. This strategic enforcement is key in preventing tunneling issues that commonly occur in simulations with insufficient constraint handling.

This new scheme differs from the traditional backtracking line search method in a critical aspect: it initializes the step size. Instead of starting with a step size of \(1\), the filter line search method begins with \(\alpha^C\). This modification is subtle yet significant.

Algorithm 8.2.1 (Filter Backtracking Line Search).

Remark 8.2.1 (Algorithm Dependency Issue). Using the optimization path to approximate the motion trajectory is still not perfect as it is algorithm dependent. Other than the projected Newton (PN) method, there could be an algorithm that walks around an obstacle and ended up with a configuration on the other side, still providing a tunneling solution (Figure 8.2.3). Even with projected Newton, although in practice it always generates straightforward and physically plausible trajectories, there is no theoretical guarantee that it will never encounter tunneling issues. An intuition is that the search direction in every PN iteration always significantly decreases the Incremental Potential (IP), and so it is unlikely to walk around any contacts which often results in iterations that do not sufficiently decrease the IP. In fact, this kind of issue also happens in elastodynamics simulation without contact. Elasticity energy itself is also nonconvex, which can result in multiple local optima for the IP. The key to obtaining physical behaviors is to locally minimize IP, in other words, finding the nearby local minimum as the solution.

Figure 8.2.3. For the setup in the tunneling example, even with the filter line search scheme, if an optimization method other than projected Newton is applied, it could still lead to the tunneling issue.

Case Study: Square Drop

To conclude, let's consider a case study where we simulate a square dropped onto a fixed planar ground. Building on our previous mass-spring model for an elastic square, we augment a barrier potential into its Incremental Potential and apply the filter line search scheme to manage the contact between the square's degrees of freedom (DOFs) and the ground.

The excutable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial under the 3_contact folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/3_contact folder.

If we further limit the planar ground to be horizontal, e.g. at \(y=y_0\), its signed distance function can be made even simpler than Equation (7.1.1): Combining it with Equation (7.2.4) and Equation (7.2.5), we can conveniently implement the gradient and Hessian computation for the barrier potential of this horizontal ground:

Implementation 8.3.1 (Barrier energy value, gradient, and Hessian, BarrierEnergy.py).

import math
import numpy as np

dhat = 0.01
kappa = 1e5

def val(x, y_ground, contact_area):
    sum = 0.0
    for i in range(0, len(x)):
        d = x[i][1] - y_ground
        if d < dhat:
            s = d / dhat
            sum += contact_area[i] * dhat * kappa / 2 * (s - 1) * math.log(s)
    return sum

def grad(x, y_ground, contact_area):
    g = np.array([[0.0, 0.0]] * len(x))
    for i in range(0, len(x)):
        d = x[i][1] - y_ground
        if d < dhat:
            s = d / dhat
            g[i][1] = contact_area[i] * dhat * (kappa / 2 * (math.log(s) / dhat + (s - 1) / d))
    return g

def hess(x, y_ground, contact_area):
    IJV = [[0] * len(x), [0] * len(x), np.array([0.0] * len(x))]
    for i in range(0, len(x)):
        IJV[0][i] = i * 2 + 1
        IJV[1][i] = i * 2 + 1
        d = x[i][1] - y_ground
        if d < dhat:
            IJV[2][i] = contact_area[i] * dhat * kappa / (2 * d * d * dhat) * (d + dhat)
        else:
            IJV[2][i] = 0.0
    return IJV

For the filter line search, with the position in the last iteration \(\mathbf{x}\) and a search direction \(\mathbf{p}\) of a specific node, the signed distance function is simply \[ d(\mathbf{x} + \alpha \mathbf{p}) = \mathbf{x}_y + \alpha \mathbf{p}_y - y_0, \] where \(\alpha\) is the step size, and there is only one positive real root \(\alpha = (y_0 - \mathbf{x}_y) / \mathbf{p}_y\) when \(\mathbf{p}_y < 0\) since \(\mathbf{x}_y > y_0\) (no interpenetration up to current iteration). Taking the minimum of the positive real root per node then gives us the step size upper bound \(\alpha_C\) defined in Equation (8.2.1):

Implementation 8.3.2 (Ground CCD, BarrierEnergy.py).

def init_step_size(x, y_ground, p):
    alpha = 1
    for i in range(0, len(x)):
        if p[i][1] < 0:
            alpha = min(alpha, 0.9 * (y_ground - x[i][1]) / p[i][1])
    return alpha

Here we scale the upper bound by \(0.9\times\) so that exact touching configurations with \(d=0\) and \(b = \infty\) (floating-point number overflow) can be avoided.

Then once we make sure the step size upper bound is used to initialize the line search

Implementation 8.3.3 (Filter line search, time_integrator.py).

        # filter line search
        alpha = BarrierEnergy.init_step_size(x, y_ground, p)  # avoid interpenetration and tunneling
        while IP_val(x + alpha * p, e, x_tilde, m, l2, k, y_ground, contact_area, h) > E_last:
            alpha /= 2

and that the contact area weights for all nodes are calculated

Implementation 8.3.4 (Contact area, simulator.py).

contact_area = [side_len / n_seg] * len(x)     # perimeter split to each node

and passed to our simulator, we can simulate the square drop with mass-spring stiffness k=2e4 and time step size h=0.01 as shown in Figure 8.3.1.

Figure 8.3.1. A mass-spring elastic square is dropped onto the ground with initial velocity under gravity. Here we show the frames when the square is: just dropped, first touching the ground, compressed to the maximum in this simulation, and becoming static.

Remark 8.3.1 (Contact Layer Integration). Since in practice, contact forces are only exerted on the boundary of the solids, the barrier potential should be integrated only on the boundary as well. This also explains why in our case study the contact area weight per node is simply calculated as the diameter of the square evenly distributed onto each boundary node. However, as mass-spring elasticity cannot guarantee that all interior nodes will stay inside the boundary of the solid, we simply apply the barrier potential to all nodal DOFs of the square.

Summary

To mitigate tunneling issues in solid simulation with large time steps, it is crucial to enforce non-negativity constraints of signed distances between solids and obstacles throughout the entire motion trajectory, not just at the final solution.

While directly using the optimization path to approximate the motion trajectory isn't perfect theoretically, it supports the design of a filter line search scheme. This scheme utilizes continuous collision detection (CCD) and the projected Newton method, effectively preventing tunneling in practical scenarios.

The projected Newton method, a gradient-based approach for minimizing the Incremental Potential, requires that the potential energy has a continuous gradient. Consequently, the distance functions employed in our barrier potential need to be at least continuous. For grid-based signed distance fields (Example 7.1.3), mere bilinear interpolation is considered insufficient.

Additionally, handling self-contact on the piece-wise linear boundary of a mesh necessitates further approximations to smooth the distance function. Detailed exploration of self-contact will be addressed in future sections

add ref

. Before that, we will first transition to discussing solids-obstacle friction in our next lecture.

Frictional Contact

In the macroscopic view, contact forces comprise not only the normal forces that prevent interpenetrations but also tangential friction forces that dampen shearing motions at the interfaces. Most surfaces, when observed microscopically, are not perfectly smooth but are formed of jagged edges. Friction essentially arises from forces preventing non-interpenetration between these jagged edges. In this lecture, we introduce the Coulomb friction model, incorporating approximations that make it compatible with optimization time integrators.

Smooth Dynamic-Static Transition

To model frictional contact, local frictional forces can be added for every active contact point pair . For each such pair , at the current state , a consistently oriented sliding basis can be constructed, where is the total number of simulated nodes and is the dimension of space, such that provides the local relative sliding velocity that is orthogonal to the distance gradient in the normal direction .

Example 9.1.1 (Particle Sliding on Sphere). For a particle with velocity moving on the surface of a sphere with velocity (no rotation), the relative sliding velocity here can be calculated as If we stack the velocity of the particle and the sphere for this system to obtain , we now know that is simply For more general cases like mesh-mesh contact, the form of only varies in how the relative velocity at the contact point pair is related to the velocity at the simulated nodes.

Maximizing dissipation rate subject to the Coulomb constraint defines friction forces variationally where is the contact force magnitude and is the local friction coefficient. This is equivalent to with when , while takes any unit vector orthogonal to when . In addition, the friction scaling function, , is also nonsmooth with respect to since when , and when . These non-smoothness would severely slow down and even break convergence of gradient-based optimization.

Figure 9.1.1. An illustration of , , , and when a point slides on a sphere.

Remark 9.1.1 (Contact Force Magnitude). is the contact force magnitude because at node , the contact force is . Therefore, since and .

To enable efficient and stable optimization, the friction-velocity relation in the transition to static friction can be mollified by replacing with a smoothly approximated function. Following IPC, we use where and a velocity magnitude bound (in units of ) below which sliding velocities are treated as static is defined for bounded approximation error (Figure 9.1.2).

Figure 9.1.2. A 1D illustration of the smoothed relation between friction force and sliding velocity. Decreasing asymptotically matches the discontinuous Coulomb friction model.

Semi-Implicit Discretization

However, challenges still remain on incorporating friction into the optimization time integration. A major problem is that friction is not a conservative force and there is no well-defined potential such that taking the opposite of its gradient produces the frictional force. In other words, implicit friction force is not integrable. Without a potential energy, backtracking line search could not be performed, and thus guarantees on the stability and convergence of the optimization will be broken.

In fact, whether a force has well-defined potential energy really depends on the temporal discretization. For example, with explicit time integration, any force is constant within a time step and it has a potential energy . Taking this inspiration, we could make friction force integrable with a smarter temporal discretization. Making friction force constant within a time step would certainly restrict the size of the time step to obtain high quality results. Therefore, we discretize part of the friction force explicitly and formulate an integrable semi-implicit friction force.

Following IPC, we fix the normal force magnitude (the ones only used in calculating friction) and the tangent operator during the nonlinear optimization to the value in the last time step : , and , which then makes the friction force integrable with a potential energy where , , and so that . Here is a constant multiple of the time step size for most linear (multi-)step time integration methods including implicit Euler and higher-order backward difference formulas, etc. Then, taking the gradient of Equation (9.2.1) w.r.t. we obtain which is a semi-implicit discretization of our mollified friction force with explicit terms and . The Hessian of can be calculated as

Remark 9.2.1. In the friction gradient and Hessian expression (Equation (9.2.3) and Equation (9.2.4)), there are in the denominators, which could be when there is no relative sliding motion at a contact point. To avoid division by during the computation, for friction gradient, we can derive which is well-defined everywhere, and so we obtain For friction Hessian, we can derive which is also well-defined everywhere, and since when , we know that

Remark 9.2.2. The friction formulation in this lecture is introduced slightly differently from the original IPC [Li et al. 2020] in 2 places:

  1. We directly use the relative sliding velocity rather than the relative sliding displacement in IPC as the input to the mollifier , and so our differs from that in the IPC on in the denominators. When time integration rules other than implicit Euler is applied (so ), calling the relative sliding displacement is inappropriate and may cause confusions.
  2. We did not introduce a tangent basis to express relative sliding velocity in the tangent space, because this is not necessary in computing the friction energy, gradient, and Hessian.

Fixed-Point Iteration

To obtain the solution with fully implicit friction, we can iteratively alternate between the nonlinear optimization with fixed , and given as and friction update until convergence (Algorithm 9.3.1).

Algorithm 9.3.1 (Fixed-Point Iteration for Fully-Implicit Friction).

If we denote \begin{equation} \begin{aligned} & f_m({ \lambda, T }) = \text{arg}\min_x E(x, { \lambda, T}) \ & f_u(x) = \text{FrictionUpdate}(x), \end{aligned} \end{equation} then Algorithm 9.3.1 is essentially a fixed-point iteration that finds the fixed-point of function \begin{equation} (f_m \cdot f_u) (x) \equiv f_m( f_u (x)). \end{equation}

Definition 9.3.1. is a fixed point of function if and only if \begin{equation} x = f(x). \end{equation} The fixed-point iterations find the fixed-point of a function starting from by iteratively updating the estimate \begin{equation} x^{i+1} \leftarrow f(x^i) \end{equation} until convergence.

Since the convergence of fixed-point iterations could only be achieved given an initial guess sufficiently close to the final solution, the convergence of Algorithm 9.3.1 analogously requires small time step sizes. However, note that each minimization with fixed (Algorithm 9.3.1 line 4) is still guaranteed to converge with arbitrarily large time step sizes.

Remark 9.3.1. In practice, semi-implicit friction with frame-rate time step sizes can already produce results with high visual quality. For higher accuracy, running 2 to 3 fixed-point iterations for friction is generally sufficient.

Summary

We introduced the Coulomb friction model, which non-smoothly penalizes shearing motion at contact points through static and dynamic friction forces in the tangent space.

To integrate friction into the optimization time integrator, we first smoothly approximate the dynamic-static transition. This allows friction forces to be uniquely determined using only the nodal velocity degrees of freedom.

We then apply a semi-implicit discretization that fixes the normal force magnitude and the tangent operator at the previous time step, enhancing the integrability of friction.

To achieve a solution with fully-implicit friction, fixed-point iterations are performed. These iterations alternate between semi-implicit time integration and updates for and .

In the next lecture, we will explore a case study involving a square on a slope with varying friction coefficients.

Case Study: Square On Slope*

In this section, based on our learnings from Frictional Contact, we implement frictional contact for a slope within the optimization time integration framework. We start by extending the contact model used for horizontal grounds in the Square Drop case study to accommodate slopes with arbitrary orientations and locations.

Following this extension, we implement friction for the slope, tested by simulating an elastic square dropped onto it. Depending on the friction coefficient , the square either stops at various points on the slope or continues to slide.

The excutable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial under the 4_friction folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/4_friction folder.

From Ground to Slope

The implementation in the Square Drop case study for horizontal grounds results in a simplified distance and distance gradient (Equation (8.3.1)) compared to that of a general half-space (Equation (7.1.1)): This is all we need for implementing the slope. Defining a normal direction and a point lying on the slope

Implementation 10.1.1 (Slope setup, simulator.py).

ground_n = np.array([0.1, 1.0])     # normal of the slope
ground_n /= np.linalg.norm(ground_n)    # normalize ground normal vector just in case
ground_o = np.array([0.0, -1.0])    # a point on the slope  

and passing them to the time integrator and barrier energy, we can modify the barrier energy value, gradient, and Hessian computation for the slope as

Implementation 10.1.2 (Slope contact barrier, BarrierEnergy.py).

import math
import numpy as np

dhat = 0.01
kappa = 1e5

def val(x, n, o, contact_area):
    sum = 0.0
    for i in range(0, len(x)):
        d = n.dot(x[i] - o)
        if d < dhat:
            s = d / dhat
            sum += contact_area[i] * dhat * kappa / 2 * (s - 1) * math.log(s)
    return sum

def grad(x, n, o, contact_area):
    g = np.array([[0.0, 0.0]] * len(x))
    for i in range(0, len(x)):
        d = n.dot(x[i] - o)
        if d < dhat:
            s = d / dhat
            g[i] = contact_area[i] * dhat * (kappa / 2 * (math.log(s) / dhat + (s - 1) / d)) * n
    return g

def hess(x, n, o, contact_area):
    IJV = [[0] * 0, [0] * 0, np.array([0.0] * 0)]
    for i in range(0, len(x)):
        d = n.dot(x[i] - o)
        if d < dhat:
            local_hess = contact_area[i] * dhat * kappa / (2 * d * d * dhat) * (d + dhat) * np.outer(n, n)
            for c in range(0, 2):
                for r in range(0, 2):
                    IJV[0].append(i * 2 + r)
                    IJV[1].append(i * 2 + c)
                    IJV[2] = np.append(IJV[2], local_hess[r, c])
    return IJV

Then for the continuous collision detection, we similarly modify the implementation to compute the large feasible initial step size for line search using and :

Implementation 10.1.3 (Slope CCD, BarrierEnergy.py).

def init_step_size(x, n, o, p):
    alpha = 1
    for i in range(0, len(x)):
        p_n = p[i].dot(n)
        if p_n < 0:
            alpha = min(alpha, 0.9 * n.dot(x[i] - o) / -p_n)
    return alpha

Here the search direction of each node is projected onto the normal direction to divide the current distance when computing the smallest step size that first brings the distance to .

Finally, drawing the slope as a line from to where pointing to the inclined direction,

Implementation 10.1.4 (Slope visualization, simulator.py).

    pygame.draw.aaline(screen, (0, 0, 255), screen_projection([ground_o[0] - 3.0 * ground_n[1], ground_o[1] + 3.0 * ground_n[0]]), 
        screen_projection([ground_o[0] + 3.0 * ground_n[1], ground_o[1] - 3.0 * ground_n[0]]))   # slope

we can now simulate an elastic square dropped on a slope without friction (Figure 10.1.1).

Figure 10.1.1. An elastic square dropped onto a frictionless slope, bouncing as it slides down.

Slope Friction

Now to implement friction for the slope, we start by implementing the functions that calculate , , and according to Equation (9.2.2), Equation (9.2.5), and Equation (9.2.6) respectively.

Implementation 10.2.1 (Friction helper functions, FrictionEnergy.py).

import numpy as np
import utils

epsv = 1e-3

def f0(vbarnorm, epsv, hhat):
    if vbarnorm >= epsv:
        return vbarnorm * hhat
    else:
        vbarnormhhat = vbarnorm * hhat
        epsvhhat = epsv * hhat
        return vbarnormhhat * vbarnormhhat * (-vbarnormhhat / 3.0 + epsvhhat) / (epsvhhat * epsvhhat) + epsvhhat / 3.0

def f1_div_vbarnorm(vbarnorm, epsv):
    if vbarnorm >= epsv:
        return 1.0 / vbarnorm
    else:
        return (-vbarnorm + 2.0 * epsv) / (epsv * epsv)

def f_hess_term(vbarnorm, epsv):
    if vbarnorm >= epsv:
        return -1.0 / (vbarnorm * vbarnorm)
    else:
        return -1.0 / (epsv * epsv)

With these terms available, we can then implement the semi-implicit friction energy value, gradient, and Hessian computations according to Equation (9.2.1), Equation (9.2.3), and Equation (9.2.4) respectively.

Implementation 10.2.2 (Friction value, gradient, and Hessian, FrictionEnergy.py).

def val(v, mu_lambda, hhat, n):
    sum = 0.0
    T = np.identity(2) - np.outer(n, n) # tangent of slope is constant
    for i in range(0, len(v)):
        if mu_lambda[i] > 0:
            vbar = np.transpose(T).dot(v[i])
            sum += mu_lambda[i] * f0(np.linalg.norm(vbar), epsv, hhat)
    return sum

def grad(v, mu_lambda, hhat, n):
    g = np.array([[0.0, 0.0]] * len(v))
    T = np.identity(2) - np.outer(n, n) # tangent of slope is constant
    for i in range(0, len(v)):
        if mu_lambda[i] > 0:
            vbar = np.transpose(T).dot(v[i])
            g[i] = mu_lambda[i] * f1_div_vbarnorm(np.linalg.norm(vbar), epsv) * T.dot(vbar)
    return g

def hess(v, mu_lambda, hhat, n):
    IJV = [[0] * 0, [0] * 0, np.array([0.0] * 0)]
    T = np.identity(2) - np.outer(n, n) # tangent of slope is constant
    for i in range(0, len(v)):
        if mu_lambda[i] > 0:
            vbar = np.transpose(T).dot(v[i])
            vbarnorm = np.linalg.norm(vbar)
            inner_term = f1_div_vbarnorm(vbarnorm, epsv) * np.identity(2)
            if vbarnorm != 0:
                inner_term += f_hess_term(vbarnorm, epsv) / vbarnorm * np.outer(vbar, vbar)
            local_hess = mu_lambda[i] * T.dot(utils.make_PSD(inner_term)).dot(np.transpose(T)) / hhat
            for c in range(0, 2):
                for r in range(0, 2):
                    IJV[0].append(i * 2 + r)
                    IJV[1].append(i * 2 + c)
                    IJV[2] = np.append(IJV[2], local_hess[r, c])
    return IJV

Note that in Numpy, matrix-matrix and matrix-vector products are realized by the dot() function. For implicit Euler, and so . Here mu_lambda stores for each node, where the normal force magnitude is calculated using at the beginning of each time step.

Implementation 10.2.3 (Use mu and lambda, time_integrator.py).

def step_forward(x, e, v, m, l2, k, n, o, contact_area, mu, is_DBC, h, tol):
    x_tilde = x + v * h     # implicit Euler predictive position
    x_n = copy.deepcopy(x)
    mu_lambda = BarrierEnergy.compute_mu_lambda(x, n, o, contact_area, mu)  # compute mu * lambda for each node using x^n

    # Newton loop

Implementation 10.2.4 (Compute mu and lambda, BarrierEnergy.py).

def compute_mu_lambda(x, n, o, contact_area, mu):
    mu_lambda = np.array([0.0] * len(x))
    for i in range(0, len(x)):
        d = n.dot(x[i] - o)
        if d < dhat:
            s = d / dhat
            mu_lambda[i] = mu * -contact_area[i] * dhat * (kappa / 2 * (math.log(s) / dhat + (s - 1) / d))
    return mu_lambda

Since the slope is static, and the normal direction is the same everywhere, is constant and so can be discretized accurately.

Finally, we set friction coefficient and pass it to the time integrator where we add friction energy to model semi-implicit friction on the slope.

mu = 0.11        # friction coefficient of the slope

Now we are ready to test the simulation with different friction coefficients. Since our slope has an inclined angle with , we test , , and (Figure 10.2.1). Here we see that when , the critical value that provides dynamic friction forces in the same magnitude with that of the gravity component on the slope, the square keeps sliding after gaining the initial momentum (Figure 10.2.1 top). When we set , right above the critical value, the square slides a while and then stopped, showing that static friction is properly resolved (Figure 10.2.1 middle). With , the square stops even earlier (Figure 10.2.1 bottom).

Figure 10.2.1. With friction coefficient (top), (middle), and (bottom), we simulate an elastic square dropped onto a slope. Except the top one that the square keeps sliding, the lower two with larger both end up with a static equilibrium.

Summary

In this case study, we implemented semi-implicit friction between simulated objects and a slope, accommodating arbitrary orientations and positions. Within the optimization time integration framework of IPC, friction is also modeled using potential energy. The key difference is that the normal force magnitude and tangent operator are precomputed at the start of each time step for semi-implicit discretization.

In the next lecture, we will introduce moving boundary conditions. This will involve obstacles or boundary nodes moving in a prescribed manner, actively injecting dynamics into the scene.

Moving Boundary Conditions*

Kinematic Collision Objects (CO) and Moving Dirichlet Boundary Conditions (BC) are crucial in many simulation scenarios. A CO can be considered as a collection of BC nodes.

At the start of a time step, it is ideal if the BC nodes can be moved directly to their prescribed locations without causing any interpenetrations. This allows the simulation to proceed smoothly using the Degree of Freedom (DOF) elimination method, which ensures the constraints remain feasible.

However, with large time steps, high velocities, or significant deformations, directly prescribing BC nodes often leads to interpenetration or "tunneling" artifacts, where objects pass through each other unrealistically.

To address these challenges, the penalty method is applied. This method progressively adjusts the simulation towards a feasible set where both CO and BC constraints are satisfied, and interpenetrations are avoided.

A case study demonstrating these principles will be shown through the simulation of a compressed square.

Penalty Method

At the beginning of each time step towards time , we evaluate nodal position for each BC node based on their prescribed motions. During each Newton iteration , for the iterate , we define a velocity residual to assess how close each BC node is to meeting its target: When falls below a specific tolerance for any BC node , we can fix the node at its current location and apply the DOF elimination method in the subsequent iterations. This is particularly straightforward in scenes with only static BCs, where the DOF elimination method is directly applied.

For other BC nodes that are far from their target locations, we introduce new penalty terms to the Incremental Potential for each of these nodes: Here, represents the nodal mass, allowing for intuitive setting of the penalty stiffness , as the Hessian of the penalty term with respect to BC nodes is simply times that of the inertia term.

Remark 11.1.1. For collision obstacles (CO), precisely calculating node masses is challenging due to unknown factors like density. A practical approach is to assume a density similar to that of the simulated solids in the scene. This assumption makes the diagonal entries on the Hessian of the penalty terms roughly times that of the inertia term.

For codimensional COs such as shells, rods, and particles, the key is to consider a reasonably large thickness when calculating their volumes. This helps in ensuring that their physical properties align more closely with those of the main simulation bodies.

Setting the penalty stiffness appropriately can be challenging. If is set too low, it may not effectively move the BC node towards its target. Conversely, a too high can lead to numerical issues. Thus, we initially set to a reasonably large value and adaptively increase it as necessary.

During the Newton solve, if there are BC nodes where at the point of Newton convergence, we double the penalty stiffness to its current value and continue the Newton solve. This process is repeated until all BCs are satisfactorily met at convergence.

Remark 11.1.2. In practice, with double precision floating-point numbers, initializing below is typically sufficient, given that the Hessian of the stiff penalty terms is purely diagonal. However, if certain BCs remain unsatisfied even when is increased to above , the optimization process may stall due to severe numerical errors. This stalling occurs because extremely stiff penalty terms are in conflict with the contact barriers. However, such a scenario would likely only occur under a rare CO/BC setting in a manner far more extreme than what is tested in Figure 2.3.1.

Case Study: Compressing Square

We simulate compressing an elastic square using a ceiling. The excutable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial under the 5_mov_dirichlet folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/5_mov_dirichlet folder.

The ceiling in our simulation is modeled as a half-space with a downward normal vector . The distance from the ceiling to other simulated Degrees of Freedom (DOFs) can be calculated using Equation (7.1.1). To effectively apply the penalty method, it's necessary that the ceiling's height also serves as a DOF.

Following the approach used in the Square on Slope project, we choose the origin on the ceiling as the DOF and incorporate it into the variable :

Implementation 11.2.1 (Ceiling DOF setup, simulator.py).

[x, e] = square_mesh.generate(side_len, n_seg)      # node positions and edge node indices
x = np.append(x, [[0.0, side_len * 0.6]], axis=0)   # ceil origin (with normal [0.0, -1.0])

The ceiling is initially positioned directly above the elastic square, as shown in the left image of Figure 11.2.1. By doing so, we ensure that the nodal mass of this newly added DOF is consistent with the other simulated nodes on the square, as per our implementation.

With this additional DOF, we can straightforwardly model the contact between the ceiling and the square. This is done by enhancing the existing functions that compute the barrier energy value, gradient, Hessian, and the initial step size:

Implementation 11.2.2 (Barrier energy value, BarrierEnergy.py).

    n = np.array([0.0, -1.0])
    for i in range(0, len(x) - 1):
        d = n.dot(x[i] - x[-1])
        if d < dhat:
            s = d / dhat
            sum += contact_area[i] * dhat * kappa / 2 * (s - 1) * math.log(s)

Implementation 11.2.3 (Barrier energy gradient, BarrierEnergy.py).

    n = np.array([0.0, -1.0])
    for i in range(0, len(x) - 1):
        d = n.dot(x[i] - x[-1])
        if d < dhat:
            s = d / dhat
            local_grad = contact_area[i] * dhat * (kappa / 2 * (math.log(s) / dhat + (s - 1) / d)) * n
            g[i] += local_grad
            g[-1] -= local_grad

Implementation 11.2.4 (Barrier energy Hessian, BarrierEnergy.py).

    n = np.array([0.0, -1.0])
    for i in range(0, len(x) - 1):
        d = n.dot(x[i] - x[-1])
        if d < dhat:
            local_hess = contact_area[i] * dhat * kappa / (2 * d * d * dhat) * (d + dhat) * np.outer(n, n)
            index = [i, len(x) - 1]
            for nI in range(0, 2):
                for nJ in range(0, 2):
                    for c in range(0, 2):
                        for r in range(0, 2):
                            IJV[0].append(index[nI] * 2 + r)
                            IJV[1].append(index[nJ] * 2 + c)
                            IJV[2] = np.append(IJV[2], ((-1) ** (nI != nJ)) * local_hess[r, c])

Implementation 11.2.5 (Initial step size calculation, BarrierEnergy.py).

    n = np.array([0.0, -1.0])
    for i in range(0, len(x) - 1):
        p_n = (p[i] - p[-1]).dot(n)
        if p_n < 0:
            alpha = min(alpha, 0.9 * n.dot(x[i] - x[-1]) / -p_n)

Here for the distance between the ceiling and a node , we have the stacked quantities locally:

Now we apply the moving BC on the ceiling to compress the elastic square. We set the ceiling's DOF, identified by the node index (n_seg+1)*(n_seg+1), as the sole Dirichlet Boundary Condition (DBC) in this scene. We assign it a downward velocity of . The movement is stopped when the ceiling reaches a height of :

Implementation 11.2.6 (DBC setup, simulator.py).

DBC = [(n_seg + 1) * (n_seg + 1)]       # dirichlet node index
DBC_v = [np.array([0.0, -0.5])]         # dirichlet node velocity
DBC_limit = [np.array([0.0, -0.6])]     # dirichlet node limit position

Then we implement the penalty term according to Equation (11.1.1), which is essentially a quadratic spring energy for controlling the motion of the ceiling:

Implementation 11.2.7 (Spring energy computation, SpringEnergy.py).

import numpy as np

def val(x, m, DBC, DBC_target, k):
    sum = 0.0
    for i in range(0, len(DBC)):
        diff = x[DBC[i]] - DBC_target[i]
        sum += 0.5 * k * m[DBC[i]] * diff.dot(diff)
    return sum

def grad(x, m, DBC, DBC_target, k):
    g = np.array([[0.0, 0.0]] * len(x))
    for i in range(0, len(DBC)):
        g[DBC[i]] = k * m[DBC[i]] * (x[DBC[i]] - DBC_target[i])
    return g

def hess(x, m, DBC, DBC_target, k):
    IJV = [[0] * 0, [0] * 0, np.array([0.0] * 0)]
    for i in range(0, len(DBC)):
        for d in range(0, 2):
            IJV[0].append(DBC[i] * 2 + d)
            IJV[1].append(DBC[i] * 2 + d)
            IJV[2] = np.append(IJV[2], k * m[DBC[i]])
    return IJV

Next, we focus on optimizing with the spring energies while properly handling the convergence check and penalty stiffness adjustments. At the start of each time step, the target position for each DBC node is computed, and the penalty stiffness, , is initialized to . If certain nodes reach their preset limit, we then set the target as their current position:

Implementation 11.2.8 (DBC initialization, time_integrator.py).

    DBC_target = [] # target position of each DBC in the current time step
    for i in range(0, len(DBC)):
        if (DBC_limit[i] - x_n[DBC[i]]).dot(DBC_v[i]) > 0:
            DBC_target.append(x_n[DBC[i]] + h * DBC_v[i])
        else:
            DBC_target.append(x_n[DBC[i]])
    DBC_stiff = 10  # initialize stiffness for DBC springs

Entering the Newton loop, in each iteration, just before computing the search direction, we assess how many DBC nodes are close enough to their target positions. We store these results in the variable DBC_satisfied:

Implementation 11.2.9 (DBC satisfaction check, time_integrator.py).

    # check whether each DBC is satisfied
    DBC_satisfied = [False] * len(x)
    for i in range(0, len(DBC)):
        if LA.norm(x[DBC[i]] - DBC_target[i]) / h < tol:
            DBC_satisfied[DBC[i]] = True

Then we only eliminate the DOFs of those DBC nodes that already satisfy the boundary condition:

Implementation 11.2.10 (DOF elimination, time_integrator.py).

    # eliminate DOF if it's a satisfied DBC by modifying gradient and Hessian for DBC:
    for i, j in zip(*projected_hess.nonzero()):
        if (is_DBC[int(i / 2)] & DBC_satisfied[int(i / 2)]) | (is_DBC[int(j / 2)] & DBC_satisfied[int(i / 2)]): 
            projected_hess[i, j] = (i == j)
    for i in range(0, len(x)):
        if is_DBC[i] & DBC_satisfied[i]:
            reshaped_grad[i * 2] = reshaped_grad[i * 2 + 1] = 0.0
    return [spsolve(projected_hess, -reshaped_grad).reshape(len(x), 2), DBC_satisfied]

The BC satisfaction information stored in DBC_satisfied is also used to check convergence and update when needed:

Implementation 11.2.11 (Convergence criteria, time_integrator.py).

    [p, DBC_satisfied] = search_dir(x, e, x_tilde, m, l2, k, n, o, contact_area, (x - x_n) / h, mu_lambda, is_DBC, DBC, DBC_target, DBC_stiff, tol, h)
    while (LA.norm(p, inf) / h > tol) | (sum(DBC_satisfied) != len(DBC)):   # also check whether all DBCs are satisfied
        print('Iteration', iter, ':')
        print('residual =', LA.norm(p, inf) / h)

        if (LA.norm(p, inf) / h <= tol) & (sum(DBC_satisfied) != len(DBC)):
            # increase DBC stiffness and recompute energy value record
            DBC_stiff *= 2
            E_last = IP_val(x, e, x_tilde, m, l2, k, n, o, contact_area, (x - x_n) / h, mu_lambda, DBC, DBC_target, DBC_stiff, h)

Now, we proceed to run the simulation, which involves severely compressing the dropped elastic square as depicted in (Figure 11.2.1). From the final static frame, we observe that the elastic springs on the edges are inverted due to extreme compression. This artifact is typical in mass-spring models of elasticity. In future chapters, we will explore how applying finite-element discretization to barrier-type elasticity models, such as the Neo-Hookean model, can prevent such issues. That approach is akin to the enforcement of non-interpenetrations in our current simulations.

Figure 11.2.1. A square is dropped onto the ground and compressed by a ceiling until inverted.

Summary

We introduced the penalty method for handling moving boundary conditions while preventing interpenetrations. The key strategies involved are:

  • Augmenting the Incremental Potential with additional spring energies on the DBC nodes;
  • Adaptively increasing the penalty stiffness as required;
  • Eliminating DOFs for those BC nodes that are sufficiently close to their targets; and
  • Ensuring all BCs are satisfied at the point of convergence.

To address the inversion artifact observed in our case study of compressing mass-spring elastic squares, the application of barrier-type elasticity energies is essential. Our penalty method for moving BCs plays a crucial role when these energies are applied, as directly prescribing BC nodes can still lead to inversion. In the next chapter, we will explore hyperelasticity models, which are preferred over mass-spring systems in practical applications.

Kinematics Theory

In previous case studies, we've relied on the mass-spring model to simulate the elastic behaviors of solids. This model approximates 2D and 3D elasticity by connecting multiple springs in various directions, each responding only to stretch and compression. However, this simple approximation often fails to capture the complexities of real-world phenomena. Starting with this lecture, we will delve into the mathematical description of deformation and introduce a more rigorous approach to modeling elasticity for continuum bodies.

When discussing continuum bodies or continuum mechanics, we operate under the continuum assumption. This perspective treats materials—whether solid, liquid, or gas—as continuous entities, avoiding the need to account for microscopic interactions between molecules and atoms. This assumption is not only practical in engineering and graphics applications but is also prevalent in everyday scenarios.

In graphics simulations, the continuum assumption applies to a wide range of materials, including deformable objects (both elastic and plastic), muscle, flesh, cloth, hair, liquids, smoke, gas, and granular materials like sand, snow, mud, and soil. In continuum mechanics, properties such as density, velocity, and force are defined as continuous functions of position. We have explored their discrete counterparts in the Discrete Space and Time section.

Equations of motion, based on Newton's 2nd law, are solved within the spatial domain and evolved over time to simulate the dynamic behaviors of these materials.

Continuum Motion

Kinematics is the study of motion within continuum materials, focusing primarily on the changes in shape or deformation that occur, whether locally or globally, across different coordinate systems. The aim is to describe motion both qualitatively and quantitatively, which is crucial for deriving the governing equations of dynamics and mechanical responses. Notably, kinematics can often be described without the need to introduce concepts like force, stress, or even mass.

In continuum mechanics, deformation is typically represented through three key components:

  • Material (or undeformed) space : This represents the initial position of any point in the material.
  • World (or deformed) space : This indicates the current position of any point.
  • Deformation map : This function maps points from the material space to the world space, showing how the position of material points changes over time.

At the initial time , the material space and the world space coincide, meaning every point starts at its undeformed position.

Definition 12.1.1 (Deformation/Flow Map). The motion of material in continuum mechanics is determined by a mapping , where and or represents the dimension of the simulated problem (or domain). This mapping, often referred to as the flow map or the deformation map, is crucial in understanding how material points move over time.

  • Material Points : Points in the set are known as material points and are designated as .
  • Current Locations : Points in represent the location of material points at time , and are referred to as . The deformation map describes the trajectory of each material point throughout time, expressed as:

Example 12.1.1. If our object is moving with a constant speed along direction , then we have If an object undergoes some rigid motion after time (compared to time ), we will have where is a rotation matrix, and is some translation. and will likely be functions of time and the initial position , depending on the actual motion.

The mapping can be used to quantify relevant continuum-based physics. For example, the velocity of a given material point at time is and the acceleration is That is, and .

Remark 12.1.1. In the above, the velocity and acceleration are defined from the Lagrangian perspective. This means that both velocity and acceleration are functions of the material configuration and time , focusing on specific particles within the material. Physically, this implies that these measurements pertain to particles that have their own mass and have occupied some volume from the beginning of the simulation. The Lagrangian view is particularly valuable for tracking individual particle dynamics over time, offering detailed insights into how particles move, accelerate, and interact within the material under various conditions.

Deformation

We have and as material coordinates and world coordinates, respectively, each associated with domains and . For any point within , the mapping function transports it to at a specific time , represented as .

Definition 12.2.1 (Deformation Gradient). The Jacobian of the deformation map is referred to as the deformation gradient and is crucial in describing the physics of elasticity. It is commonly denoted as and defined by the relation: Discretely, this Jacobian often takes the form of a small or matrix. For materials like cloth or thin shells in 3D, might be a matrix, reflecting the 2D nature of the material space. Thus, maps every material point to a matrix that describes the deformation Jacobian at time . Using index notation, it can be expressed as:

We can compute the deformation gradient for the deformation map specified in Equation (12.1.1), where the result is the identity matrix. Similarly, for the deformation map in Equation (12.1.2), the deformation gradient equals . In both cases, the object does not undergo real deformation; these are merely examples of rigid transformations. Such deformation gradients should not lead to any internal forces within the material unless artistic effects are intentionally being pursued (such as in a cartoon).

Figure 12.2.1 (Deformation gradient).

Example 12.2.1. Intuitively, the deformation gradient indicates the extent of local deformation within a material. Consider two nearby points, and , embedded in the material at the start of the simulation (as illustrated in Figure 12.2.1). If and represent these points in the current configuration, the relationship between these points can be expressed as: This equation shows how the deformation gradient transforms the initial distance between the points into their current separation, thus quantifying the local deformation.

The determinant of the deformation gradient , commonly denoted by , is crucial because it characterizes the infinitesimal volume change during deformation. This is expressed as . The value of represents the ratio of the infinitesimal volume of the material in the deformed configuration to its original volume in . For instance, in rigid motions, which include rotations and translations, is a rotation matrix and therefore . Notably, the identity matrix, being a rotation matrix, also results in .

If , it indicates a volume increase, whereas indicates a decrease. A situation where suggests that the volume has effectively become zero, a scenario that is impossible in the real world but can occur numerically. In 3D, this indicates that the material is compressed to such an extent that it might collapse into a plane, line, or even a point without volume. Conversely, indicates material inversion. For example, in 2D, if for a triangle, it implies that one vertex has passed through the opposing edge, effectively 'inverting' the triangle and making its area negative. As seen in the Moving Boundary Conditions section, severe compression of an elastic square can lead to inversions. In such cases, serves as a direct measure of this artifact and is utilized in many elasticity models to ensure simulations are free from inversions.

Summary

Defining the flow map which transforms continuum bodies from the material space (initial configuration) to the world space (current configuration), we introduced a mathematical description of the change in shapes -- the deformation gradient ( or ), which is the Jacobian of the flow map with respect to .

When at a certain point on the continuum body is a rotation matrix, it indicates there is no deformation and, consequently, no local elasticity forces should be present. In the next lecture, we will explore how to define more realistic elastic potential energies using the deformation gradient.

Strain Energy

With the deformation gradient serving as a rigorous mathematical measure of local deformation, we can define the elastic potential energy based on to more accurately capture the elastic behaviors of solids. is measured at every local point within the solid domain. We would measure the elastic potential locally for each point and then integrate these measurements across the entire domain. This approach mirrors the process used in the 2D Mass Spring case study, where the energy of each spring, weighted by an estimated volume, was summed up in a discrete setting. Here, is also known as strain, and the elastic potential , referred to as strain energy, is derived from integrating strain energy density functions at each material point within the solid domain: In this lecture, we will explore various design choices of and examine some of their properties.

Rigid Null Space and Rotation Invariance

As mentioned in the previous lecture, for a solid undergoing only translational and/or rotational motions, no elastic potential energy is stored, and thus no elasticity force is exerted. This implies that any strain energy density functions have a rigid null space, meaning that should remain if the input deformation gradient is any rotation matrix : A square matrix is a rotation matrix if and only if: From this definition, a straightforward formulation for emerges, penalizing any deviation of from being a rotation matrix with quadratic terms: Here, and are the stiffness parameters, with the first term derived from right-multiplying to both sides of . This intuitive formulation closely aligns with how many standard strain energy density functions are constructed.

Definition 13.1.1 (Neo-Hookean Elasticity). The Neo-Hookean elasticity model is characterized by the following energy density function: Taking the derivative of with respect to , we obtain: From this gradient, it is evident that the -term achieves a local minimum when (i.e., ), and for the -term, the local minimum occurs at .

Definition 13.1.2 (Lame Parameters). In standard strain energy density functions, the stiffness parameters and are known as Lame parameters. These parameters are directly related to the Young's modulus , which measures resistance to stretching, and the Poisson's ratio , which measures the incompressibility of the solid:

Definition 13.1.3 (Rotation Invariance). The energy density function for any nonlinear elastic model is rotation invariant. Mathematically, this is expressed as: Intuitively, this means that any rotations applied after deformation should not alter the value of the strain energy density function.

However, the simplest strain energy density function, linear elasticity, does not include rigid modes in its null space nor does it satisfy Equation (13.1.3). This is because linear elasticity is specifically designed for infinitesimal strains, where no significant rotations are involved.

Definition 13.1.4 (Linear Elasticity). Linear elasticity has the energy density function Here is the small strain tensor, and we see that is a quadratic function of .

Notably, the linear elasticity model with the corresponding Lame parameters is calibrated to real-world experiments under conditions of small deformations. In such circumstances, all standard strain energy density functions must align with linear elasticity. The consistency between these models and linear elasticity will be concisely demonstrated after we introduce the polar singular value decomposition of in the next section.

Rotation invariance (Equation (13.1.3)) should not be confused with the isotropic property of certain elastic models.

Definition 13.1.5 (Isotropic Elasticity). The energy density function of isotropic elastic models satisfies This implies that the same amount of stretch in any direction results in the same energy change. Consequently, there are no special directions in which the material is harder or easier to deform than others.

Neo-Hookean (Equation (13.1.2)) and our intuitive model (Equation (13.1.1)) are both examples of isotropic models. However, linear elasticity (Equation (13.1.4)) does not meet this condition (Equation (13.1.5)), as it is not designed to handle rotational motions effectively.

For anisotropic elastic models, the resistance to stretch varies depending on the direction. Materials such as cloth, bones, muscles and wood are examples of anisotropic materials, exhibiting different mechanical properties in different directions.

Polar Singular Value Decomposition

When discussing general slip boundary conditions, we introduced the usage of singular value decomposition (SVD). Here, we apply a variant known as Polar SVD (Algorithm 13.2.1) to decompose : where and are both rotation matrices, and is a diagonal matrix. Unlike standard SVD, which ensures remains non-negative possibly at the expense of having or , Polar SVD maintains and , allowing to be negative if necessary.

Polar SVD is named for its relation to Polar decomposition, where is expressed as . This decomposition can be reconstructed via and , with representing the closest rotation to and being symmetric.

Algorithm 13.2.1 (Polar SVD from Standard SVD).

The Polar SVD of offers a more intuitive way to understand deformation. If we denote , referred to as the principal stretches, we can conceptualize as comprising a sequence of transformations. Initially, there is a rotation by , followed by scaling the dimensions by along each axis, and concluding with another rotation by . This decomposition is applicable for all possible .

Polar SVD also allows for the more convenient expression of isotropic strain energy density functions using exclusively. For instance, our intuitive formulation in Equation (13.1.1) can be reframed as:

where . Moreover, the Neo-Hookean strain energy density function (Equation (13.1.2)) can be rewritten as:

These two models are both consistent with linear elasticity under small deformation.

Definition 13.2.1 (Consistency to Linear Elasticity). To verify the consistency to linear elasticity of a strain energy density function , we just need to check whether the following relations all hold: Here , and if , otherwise it is .

Simplified Models and Invertibility

Definition 13.3.1 (Corotated Linear Elasticity). To make linear elasticity rotation-aware while maintaining its simplicity, we can introduce a base rotation and construct an energy density function penalizing any deviation between and this fixed . This is called corotated linear elasticity.

remains a quadratic energy with respect to and is very useful for dynamic simulations. At the beginning of the optimization for each time step , we compute as the closest rotation to : As mentioned earlier, the solution is given by the Polar decomposition on , and with Polar SVD , we have . However, corotated linear elasticity is still not rotation invariant, as does not change with during the optimization. Thus, it is not suitable for large deformations.

For rotation invariant elastic models, practitioners in computer graphics have been simplifying them for visual computing purposes. For example, only keeping a -term while ignoring the -term in the energy density function for more efficient computations: Here is called the As-Rigid-As-Possible (ARAP) energy, which is widely used in shape modeling, cloth simulation, and surface parameterization, etc. , while being a higher-order polynomial of compared to ARAP, can be computed without performing the expensive SVDs on .

For all the strain energy density functions we have looked at in this lecture, except Neo-Hookean, all others are defined on the whole domain . Neo-Hookean energy density function is defined on . Just like the barrier energy to prevent interpenetrations in IPC, is also a barrier energy, which goes to infinity as approaches , providing arbitrarily large elastic forces to prevent inversion ().

Strain energy density functions allowing are also called invertible elasticity models. They are easy to deal with (no need for line search filtering), but do not guarantee non-inversion. Designing an invertible elastic energy that provides reasonably large resistance to inversion has drawn a lot of attention in computer graphics research [Stomakhin et al. 2012] [Smith et al. 2018].

Summary

The elastic potential energy is an integration of the strain energy density function at every local point in the solid domain. From the rigid null space, we derived an intuitive formulation of the strain energy density function, similar in structure to standard models like Neo-Hookean. Nonlinear elastic models are also rotation invariant, meaning any rotations applied after the deformation do not change .

Linear elasticity features a quadratic energy density function and is specifically designed for infinitesimal strains , lacking rigid modes in its null space. Yet, with the corresponding Lame Parameters and , it can accurately capture behaviors of small deformations observed in the real world. Standard elasticity models are required to be consistent with linear elasticity under small deformations.

This lecture focused on isotropic elasticity, where no special directions exist that make the material harder or easier to deform. Performing Polar SVD on allows us to rewrite of isotropic models using only principal stretches .

Using the closest rotation to in the last time step, we constructed a corotated linear elasticity to make linear elasticity rotation-aware while maintaining its simplicity. Simplifying further by retaining only the -term enhances efficiency for visual computing.

Similar to how non-interpenetrations are enforced in IPC, the energy density function of Neo-Hookean acts as a barrier function, ensuring non-inversion (). All other elasticity models introduced in this lecture are invertible, and they do not guarantee non-inversion.

In the next lecture, we will explore the derivatives of with respect to .

Stress and Its Derivatives

Having introduced standard strain energies, we now proceed to their differentiation with respect to the world space coordinates, , to simulate realistic elastic behaviors. However, it's important to first establish the explicit relationship between these coordinates and the deformation gradient . This relationship heavily depends on specific discretization choices.

Before we explore discretization in depth, we should understand how to compute the derivatives of the strain energy function, , with respect to . These derivatives are fundamentally linked to the concept of stress, a critical element in understanding material behavior under deformation.

Stress

Stress is a tensor field, akin to the deformation gradient , and is defined over the entire domain of solid materials. It quantifies the internal pressures and tensions experienced by a material object. The link between stress and strain (or ) is established through what is known as a constitutive relationship. This relationship outlines how materials respond to various deformations.

A common example of a constitutive relationship is Hooke's law in one dimension, which applies to many conventional materials under elastic conditions. In the context of hyperelastic materials, the relationship is specifically defined by the strain energy function, .

Definition 14.1.1 (Hyperelastic Materials). Hyperelastic materials are those elastic solids whose first Piola-Kirchhoff stress can be derived from a strain energy density function via With index notation, this means is discretely a small matrix with the same dimensions as .

In the study of material behavior under stress, various definitions are utilized, with Cauchy stress being particularly prevalent in engineering contexts. Cauchy stress, denoted as , can be mathematically linked to the first Piola-Kirchhoff stress tensor through the relationship:

Calculating from the strain energy function is relatively straightforward for energy models that do not require singular value decomposition (SVD), such as the Neo-Hookean model. However, general isotropic elasticity models, like ARAP (As-Rigid-As-Possible), often rely on the computation of principal stretches or the closest rotation matrix, necessitating SVD. This computation becomes particularly complex and resource-intensive when determining , which is crucial for implicit time integrations.

We present an efficient method that leverages the sparsity structure, as introduced by [Stomakhin et al. 2012], to compute the first Piola-Kirchhoff stress tensor and its derivative (whether as a tensor or the differential ) for general isotropic elastic materials. This approach utilizes symbolic software packages, and we will specifically discuss the implementation in Mathematica. Implementations in Maple or other software are similarly straightforward, following the same conceptual framework. For a deeper exploration of derivative computations commonly employed in computer graphics, refer to the work of [Schroeder 2022].

It is important to note that the computational strategy discussed can also be applied to other derivatives in diagonal space, similar to . For instance, in certain models, the Kirchhoff stress is preferred over the first Piola-Kirchhoff stress . The Kirchhoff stress is expressed as: where is a diagonal stress measure, with each entry being a function of the singular values . The methodology for computing mirrors that of .

Computing

Let's begin with the computation of . For isotropic materials, the first Piola-Kirchhoff stress tensor can be calculated as follows: This formulation leverages the property that shares the same SVD space as , which simplifies the derivation and computation process.

Example 14.2.1. For the Neo-Hookean model (Equation (13.1.2)), we have: Thus, we can first perform SVD on and derive: to compute without symbolically deriving the derivative of w.r.t. .

Here we provide the proof that commutes with rotations in diagonal space (see Equation (14.2.1)). To demonstrate that for any rotation matrix , consider a generic (potentially anisotropic) material model. The key idea is that a rotation applied after deformation does not alter the material's stored energy, thus we have the identity . Differentiating both sides of this equation with respect to the deformation gradient yields:

Furthermore, for an isotropic material where , a similar argument shows that . Combining these relationships for under rotation, we establish that: This formulation confirms the rotational invariance of in diagonal space.

Additional Proof for

In the above, the last equality comes from the fact that Here we show why this is true.

(1) First, we claim that is diagonal. This can be seen by realizing that for isotropic elasticity, where is the isotropic invariants. Following [Sifakis & Barbic 2022] (page 23), we can observe that when the argument is diagonal, must be diagonal. Therefore, is diagonal when is diagonal.

(2) Next, we claim that This is proven in [Xu et al. 2015] (Equation 7).

(3) Based on (2), we know that for any , after substituting , we have using this we can write out the cases for . For example, for , we have

(4) Finally, let's derive . Since we know it is diagonal from (1), we just need to derive its diagonal entry. Let's use entry as an example: Now are are done with the final proof.

Computing or

To compute the derivative of with respect to , we leverage the rotational invariance property discussed earlier for . Consider two arbitrary rotation matrices and . From the rotational properties of , we have:

Define , then:

Taking the differential of , while treating and as constants, gives:

By setting and , where , the differential expression simplifies to:

The tensorial derivative is then expressed in index notation as:

These expressions must hold for any , leading to the relationship:

So the remaining task is computing . We show how to do it in 3D.

First, let's introduce Rodrigues' rotation formula, which provides a method for expressing any rotation matrix in terms of a unit vector and a rotation angle . The formula is given by: where is the skew-symmetric cross-product matrix associated with . This formula shows that any rotation matrix is characterized by just three degrees of freedom, denoted as . These components are used to define the rotation vector , from which and are derived as follows:

Using this parameterization, rotation matrices and can each be described by three parameters.

Now we have the following code for defining in terms of , , , , , , , , , where and are defined by and with Rodrigues' rotation formula, are the singular values from .

id=IdentityMatrix[3];
var={s1,s2,s3,u1,u2,u3,v1,v2,v3};
Sigma=DiagonalMatrix[{s1,s2,s3}];
cp[k1_,k2_,k3_]={{0,-k3,k2},{k3,0,-k1},{-k2,k1,0}};
vV={v1,v2,v3};
vU={u1,u2,u3};
nv=Sqrt[Dot[vV,vV]];
nu=Sqrt[Dot[vU,vU]];
UU=cp[u1,u2,u3]/nu;
VV=cp[v1,v2,v3]/nv;
U=id+Sin[nu]*UU+(1-Cos[nu])*UU.UU;
V=id+Sin[nv]*VV+(1-Cos[nv])*VV.VV;
F=U.Sigma.Transpose[V];

where cp is a function for generating the cross-product matrix (corresponding to computing in Equation (14.3.1)).

From now on, we write the tensor and any other such tensors to matrices. That means each matrix is now a size- vector. It is easy to see the old is now . We further call vector being the parametrization of . Then we can apply the chain rule

Here are the Mathematica code for computing them. Note that we achieve by taking the limit , which correspond to nearly zero rotations.

dFdS=D[Flatten[F],{var}];
dFdS0=dFdS/.{u1->e,u2->e,u3->e,v1->e,v2->e,v3->e};
dFdS1=Limit[dFdS0,e->0,Direction->-1];
dSdF0=Inverse[dFdS1];
Phat=DiagonalMatrix[{t1[s1,s2,s3],t2[s1,s2,s3],t3[s1,s2,s3]}];
P=U.Phat.Transpose[V];
dPdS=D[Flatten[P],{var}];
dPdS0=dPdS/.{u1->e,u2->e,u3->e,v1->e,v2->e,v3->e};
dPdS1=Limit[dPdS0,e->0,Direction->-1];
dPdF=Simplify[dPdS1.dSdF0];

Note 'Direction->-1' in Mathematica means taking the limit from large values to the small limit value. The Mathematica computation result will be given in terms of the singular values and . One can then take the formula for implementing them in the code. [Stomakhin et al. 2012] gives the result where (size matrix) is permuted to be a block diagonal matrix with diagonal blocks , where and Denominator clamping is needed for terms in that may introduce division-by-zero (after fully simplifying them). Here we denote and as and respectively. The division by is problematic when two singular values are nearly equal or when two singular values nearly sum to zero. The latter is possible with a convention for permitting negative singular values (as in invertible elasticity [Irving et al. 2004] [Stomakhin et al. 2012]).

Expanding in terms of partial fractions yields the useful decomposition Note that if is invariant under permutation of the singular values, then as . Thus, the first term can normally be computed robustly for an isotropic model if implemented carefully. The other fraction can be computed robustly if as . But this usually does not hold as it means the constitutive model will have difficulty recovering from degenerate or inverted configurations. Thus, this term will be unbounded under some circumstances. We address this by clamping the magnitude of the denominator to not be smaller than before division to bound the derivatives.

For 2D, a rotation matrix is now simply paremetrized with a single where the reconstruction is

The 2D version of the whole Mathematica code is

id=IdentityMatrix[2];
var={s1,s2,u1,v1};
S=DiagonalMatrix[{s1,s2}];
U={{Cos[u1],-Sin[u1]

},{Sin[u1],Cos[u1]}};
V={{Cos[v1],-Sin[v1]},{Sin[v1],Cos[v1]}};
F=U.S.Transpose[V];
dFdS=D[Flatten[F],{var}];
dFdS0=dFdS/.{u1->e,v1->e};
dFdS1=Limit[dFdS0,e->0,Direction->-1};
dSdF0=Inverse[dFdS1];
Phat=DiagonalMatrix[{t1[s1,s2],t2[s1,s2]}];
P=U.Phat.Transpose[V];
dPdS=D[Flatten[P],{var}];
dPdS0=dPdS/.{u1->e,v1->e};
dPdS1=Limit[dPdS0,e->0,Direction->-1];
dPdF=Simplify[dPdS1.dSdF0];

where is now also and there is only one .

Summary

Stress is a tensor field that quantifies the pressure or tension exerted on a material object. In the context of hyperelastic materials, the first Piola-Kirchhoff stress tensor plays a crucial role. It is defined as the derivative of the strain energy density function , with respect to the deformation gradient , establishing a constitutive relationship between stress and strain.

In practical computations, particularly for the implicit integration of solid dynamics, it is essential to compute and its derivative efficiently. By leveraging the sparsity structure in diagonal space, these computations become more feasible. Here, differentiations are primarily required for with respect to the principal stretches , which simplifies the calculation process.

In the upcoming lecture, we will apply these principles to an inversion-free elasticity model, which will be demonstrated through the compressing square simulation. This application will use the concepts discussed in this chapter to address complex real-world problems in solid mechanics.

Case Study: Inversion-free Elasticity*

At the end of this chapter, we implement the Neo-Hookean model introduced in the previous lectures to simulate inversion-free elastic solids. The excutable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial under the 6_inv_free folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/6_inv_free folder. Instead of discretizing elasticity onto the springs as in the mass-spring model, we discretize the Neo-Hookean model onto triangle elements, apply chain rules to compute elastic forces according to the relation between deformation gradient and world-space nodal position , and then develop a root-finding based approach to filter the initial step size of line search for guaranteed non-inversion.

Linear Triangle Elements

In previous discussions, we learned to calculate and its derivatives with respect to . For simulation, however, we require and . This necessitates a clear understanding of , as it allows us to employ the chain rule to derive these derivatives with respect to effectively.

In 2D simulations, we often divide the solid domain into non-degenerate triangular elements. Assume the mapping is linear within each triangle, thus keeping the deformation gradient constant. Referencing Example 12.2.1, for a triangle defined by vertices , we have the equations: where denotes the world-space coordinates of the triangle vertices. This relationship leads to the expression for : Equation (15.1.1) shows that , derived here, maps any segment within the triangle to its world-space counterpart through linear combinations of the triangle edges and . A more general and rigorous derivation of this formula will be presented in subsequent chapters.

Once is established, we can calculate its derivative with respect to for each triangle as follows: where represents the inverse of the matrix formed by subtracting the first vertex from the second and third vertices. This matrix can be precomputed at initialization along with other properties such as the volume and Lame parameters of each triangle:

Implementation 15.1.1 (Precomputation of element information, simulator.py).

# rest shape basis, volume, and lame parameters
vol = [0.0] * len(e)
IB = [np.array([[0.0, 0.0]] * 2)] * len(e)
for i in range(0, len(e)):
    TB = [x[e[i][1]] - x[e[i][0]], x[e[i][2]] - x[e[i][0]]]
    vol[i] = np.linalg.det(np.transpose(TB)) / 2
    IB[i] = np.linalg.inv(np.transpose(TB))
mu_lame = [0.5 * E / (1 + nu)] * len(e)
lam = [E * nu / ((1 + nu) * (1 - 2 * nu))] * len(e)

The Young's modulus and Poisson's ratio:

E = 1e5         # Young's modulus
nu = 0.4        # Poisson's ratio

Here, e no longer stores all edge elements as in mass-spring models but represents all triangle elements, which can be generated by modifying the meshing code as follows:

Implementation 15.1.2 (Assembling per-triangle vertex indices, square_mesh.py).

    # connect the nodes with triangle elements
    e = []
    for i in range(0, n_seg):
        for j in range(0, n_seg):
            # triangulate each cell following a symmetric pattern:
            if (i % 2)^(j % 2):
                e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])
                e.append([(i + 1) * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j + 1, i * (n_seg + 1) + j + 1])
            else:
                e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j + 1])
                e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j + 1, i * (n_seg + 1) + j + 1])

Triangles are arranged in a symmetric pattern and can be rendered by drawing the three edges:

Implementation 15.1.3 (Draw triangles, simulator.py).

        pygame.draw.aaline(screen, (0, 0, 255), screen_projection(x[eI[0]]), screen_projection(x[eI[1]]))
        pygame.draw.aaline(screen, (0, 0, 255), screen_projection(x[eI[1]]), screen_projection(x[eI[2]]))
        pygame.draw.aaline(screen, (0, 0, 255), screen_projection(x[eI[2]]), screen_projection(x[eI[0]]))

Computing Energy, Gradient, and Hessian

We first follow sections Strain Energy and Stress and Its Derivatives to implement computing , , and SPD-projected :

Implementation 15.2.1 (Energy derivatives w.r.t. , NeoHookeanEnergy.py).

import utils
import numpy as np
import math

def polar_svd(F):
    [U, s, VT] = np.linalg.svd(F)
    if np.linalg.det(U) < 0:
        U[:, 1] = -U[:, 1]
        s[1] = -s[1]
    if np.linalg.det(VT) < 0:
        VT[1, :] = -VT[1, :]
        s[1] = -s[1]
    return [U, s, VT]

def dPsi_div_dsigma(s, mu, lam):
    ln_sigma_prod = math.log(s[0] * s[1])
    inv0 = 1.0 / s[0]
    dPsi_dsigma_0 = mu * (s[0] - inv0) + lam * inv0 * ln_sigma_prod
    inv1 = 1.0 / s[1]
    dPsi_dsigma_1 = mu * (s[1] - inv1) + lam * inv1 * ln_sigma_prod
    return [dPsi_dsigma_0, dPsi_dsigma_1]

def d2Psi_div_dsigma2(s, mu, lam):
    ln_sigma_prod = math.log(s[0] * s[1])
    inv2_0 = 1 / (s[0] * s[0])
    d2Psi_dsigma2_00 = mu * (1 + inv2_0) - lam * inv2_0 * (ln_sigma_prod - 1)
    inv2_1 = 1 / (s[1] * s[1])
    d2Psi_dsigma2_11 = mu * (1 + inv2_1) - lam * inv2_1 * (ln_sigma_prod - 1)
    d2Psi_dsigma2_01 = lam / (s[0] * s[1])
    return [[d2Psi_dsigma2_00, d2Psi_dsigma2_01], [d2Psi_dsigma2_01, d2Psi_dsigma2_11]]

def B_left_coef(s, mu, lam):
    sigma_prod = s[0] * s[1]
    return (mu + (mu - lam * math.log(sigma_prod)) / sigma_prod) / 2

def Psi(F, mu, lam):
    J = np.linalg.det(F)
    lnJ = math.log(J)
    return mu / 2 * (np.trace(np.transpose(F).dot(F)) - 2) - mu * lnJ + lam / 2 * lnJ * lnJ

def dPsi_div_dF(F, mu, lam):
    FinvT = np.transpose(np.linalg.inv(F))
    return mu * (F - FinvT) + lam * math.log(np.linalg.det(F)) * FinvT

def d2Psi_div_dF2(F, mu, lam):
    [U, sigma, VT] = polar_svd(F)

    Psi_sigma_sigma = utils.make_PSD(d2Psi_div_dsigma2(sigma, mu, lam))

    B_left = B_left_coef(sigma, mu, lam)
    Psi_sigma = dPsi_div_dsigma(sigma, mu, lam)
    B_right = (Psi_sigma[0] + Psi_sigma[1]) / (2 * max(sigma[0] + sigma[1], 1e-6))
    B = utils.make_PSD([[B_left + B_right, B_left - B_right], [B_left - B_right, B_left + B_right]])

    M = np.array([[0, 0, 0, 0]] * 4)
    M[0, 0] = Psi_sigma_sigma[0, 0]
    M[0, 3] = Psi_sigma_sigma[0, 1]
    M[1, 1] = B[0, 0]
    M[1, 2] = B[0, 1]
    M[2, 1] = B[1, 0]
    M[2, 2] = B[1, 1]
    M[3, 0] = Psi_sigma_sigma[1, 0]
    M[3, 3] = Psi_sigma_sigma[1, 1]

    dP_div_dF = np.array([[0, 0, 0, 0]] * 4)
    for j in range(0, 2):
        for i in range(0, 2):
            ij = j * 2 + i
            for s in range(0, 2):
                for r in range(0, 2):
                    rs = s * 2 + r
                    dP_div_dF[ij, rs] = M[0, 0] * U[i, 0] * VT[0, j] * U[r, 0] * VT[0, s] \
                        + M[0, 3] * U[i, 0] * VT[0, j] * U[r, 1] * VT[1, s] \
                        + M[1, 1] * U[i, 1] * VT[0, j] * U[r, 1] * VT[0, s] \
                        + M[1, 2] * U[i, 1] * VT[0, j] * U[r, 0] * VT[1, s] \
                        + M[2, 1] * U[i, 0] * VT[1, j] * U[r, 1] * VT[0, s] \
                        + M[2, 2] * U[i, 0] * VT[1, j] * U[r, 0] * VT[1, s] \
                        + M[3, 0] * U[i, 1] * VT[1, j] * U[r, 0] * VT[0, s] \
                        + M[3, 3] * U[i, 1] * VT[1, j] * U[r, 1] * VT[1, s]
    return dP_div_dF

Next, we implement computing , and the tensor products with for chain rule based computation of elasticity energy gradient and Hessian:

Implementation 15.2.2 (Energy derivatives w.r.t. , NeoHookeanEnergy.py).

def deformation_grad(x, elemVInd, IB):
    F = [x[elemVInd[1]] - x[elemVInd[0]], x[elemVInd[2]] - x[elemVInd[0]]]
    return np.transpose(F).dot(IB)

def dPsi_div_dx(P, IB):  # applying chain-rule, dPsi_div_dx = dPsi_div_dF * dF_div_dx
    dPsi_dx_2 = P[0, 0] * IB[0, 0] + P[0, 1] * IB[0, 1]
    dPsi_dx_3 = P[1, 0] * IB[0, 0] + P[1, 1] * IB[0, 1]
    dPsi_dx_4 = P[0, 0] * IB[1, 0] + P[0, 1] * IB[1, 1]
    dPsi_dx_5 = P[1, 0] * IB[1, 0] + P[1, 1] * IB[1, 1]
    return [np.array([-dPsi_dx_2 - dPsi_dx_4, -dPsi_dx_3 - dPsi_dx_5]), np.array([dPsi_dx_2, dPsi_dx_3]), np.array([dPsi_dx_4, dPsi_dx_5])]

def d2Psi_div_dx2(dP_div_dF, IB):  # applying chain-rule, d2Psi_div_dx2 = dF_div_dx^T * d2Psi_div_dF2 * dF_div_dx (note that d2F_div_dx2 = 0)
    intermediate = np.array([[0.0, 0.0, 0.0, 0.0]] * 6)
    for colI in range(0, 4):
        _000 = dP_div_dF[0, colI] * IB[0, 0]
        _010 = dP_div_dF[0, colI] * IB[1, 0]
        _101 = dP_div_dF[2, colI] * IB[0, 1]
        _111 = dP_div_dF[2, colI] * IB[1, 1]
        _200 = dP_div_dF[1, colI] * IB[0, 0]
        _210 = dP_div_dF[1, colI] * IB[1, 0]
        _301 = dP_div_dF[3, colI] * IB[0, 1]
        _311 = dP_div_dF[3, colI] * IB[1, 1]
        intermediate[2, colI] = _000 + _101
        intermediate[3, colI] = _200 + _301
        intermediate[4, colI] = _010 + _111
        intermediate[5, colI] = _210 + _311
        intermediate[0, colI] = -intermediate[2, colI] - intermediate[4, colI]
        intermediate[1, colI] = -intermediate[3, colI] - intermediate[5, colI]
    result = np.array([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] * 6)
    for colI in range(0, 6):
        _000 = intermediate[colI, 0] * IB[0, 0]
        _010 = intermediate[colI, 0] * IB[1, 0]
        _101 = intermediate[colI, 2] * IB[0, 1]
        _111 = intermediate[colI, 2] * IB[1, 1]
        _200 = intermediate[colI, 1] * IB[0, 0]
        _210 = intermediate[colI, 1] * IB[1, 0]
        _301 = intermediate[colI, 3] * IB[0, 1]
        _311 = intermediate[colI, 3] * IB[1, 1]
        result[2, colI] = _000 + _101
        result[3, colI] = _200 + _301
        result[4, colI] = _010 + _111
        result[5, colI] = _210 + _311
        result[0, colI] = -_000 - _101 - _010 - _111
        result[1, colI] = -_200 - _301 - _210 - _311
    return result

Finally, Neo-Hookean energy value, gradient, and Hessian on the entire mesh can be computed as follows:

Implementation 15.2.3 (Energy value, Gradient, and Hessian, NeoHookeanEnergy.py).

def val(x, e, vol, IB, mu, lam):
    sum = 0.0
    for i in range(0, len(e)):
        F = deformation_grad(x, e[i], IB[i])
        sum += vol[i] * Psi(F, mu[i], lam[i])
    return sum

def grad(x, e, vol, IB, mu, lam):
    g = np.array([[0.0, 0.0]] * len(x))
    for i in range(0, len(e)):
        F = deformation_grad(x, e[i], IB[i])
        P = vol[i] * dPsi_div_dF(F, mu[i], lam[i])
        g_local = dPsi_div_dx(P, IB[i])
        for j in range(0, 3):
            g[e[i][j]] += g_local[j]
    return g

def hess(x, e, vol, IB, mu, lam):
    IJV = [[0] * (len(e) * 36), [0] * (len(e) * 36), np.array([0.0] * (len(e) * 36))]
    for i in range(0, len(e)):
        F = deformation_grad(x, e[i], IB[i])
        dP_div_dF = vol[i] * d2Psi_div_dF2(F, mu[i], lam[i])
        local_hess = d2Psi_div_dx2(dP_div_dF, IB[i])
        for xI in range(0, 3):
            for xJ in range(0, 3):
                for dI in range(0, 2):
                    for dJ in range(0, 2):
                        ind = i * 36 + (xI * 3 + xJ) * 4 + dI * 2 + dJ
                        IJV[0][ind] = e[i][xI] * 2 + dI
                        IJV[1][ind] = e[i][xJ] * 2 + dJ
                        IJV[2][ind] = local_hess[xI * 2 + dI, xJ * 2 + dJ]
    return IJV

Filter Line Search for Non-Inversion

To guarantee non-inversion just like non-interpenetration (see Filter Line Search) during the simulation, we can similarly filter the line search initial step size with a critical step size that first brings the volume of any triangles to . This can be obtained by solving a 1D equation per triangle: and taking the minimum of the solved step sizes. Here is the search direction of node , and in 2D, Equation (15.3.1) is equivalent to: with and , . Expanding Equation (15.3.2) we obtain: which can be reorganized as a quadratic equation of : Here, note that can be very tiny when the nodes do not move much or when their movement barely changes to triangle area in the current timestep, thus the equation can be degenerated into a linear one. To robustly detect this degenerate case, we cannot directly check whether is due to numerical errors. In fact, checking whether is below an epsilon is still tricky, because the scale of heavily depends on the scene dimension and nodal velocity during the simulation. Therefore, we use as a scaling and obtain a scaled but equivalent equation: where magnitude checks can be safely performed on any coefficients with unitless thresholds.

In practice, we also need to allow some slackness so that the step size to be taken will not lead to an exactly volume. Thus, we solve such that it first decreases the volume of any triangles by , which can be realized by modifying the constant term coefficient in Equation (15.3.3) from to :

Implementation 15.3.1 (Filter line search, NeoHookeanEnergy.py).

def init_step_size(x, e, p):
    alpha = 1
    for i in range(0, len(e)):
        x21 = x[e[i][1]] - x[e[i][0]]
        x31 = x[e[i][2]] - x[e[i][0]]
        p21 = p[e[i][1]] - p[e[i][0]]
        p31 = p[e[i][2]] - p[e[i][0]]
        detT = np.linalg.det(np.transpose([x21, x31]))
        a = np.linalg.det(np.transpose([p21, p31])) / detT
        b = (np.linalg.det(np.transpose([x21, p31])) + np.linalg.det(np.transpose([p21, x31]))) / detT
        c = 0.9  # solve for alpha that first brings the new volume to 0.1x the old volume for slackness
        critical_alpha = utils.smallest_positive_real_root_quad(a, b, c)
        if critical_alpha > 0:
            alpha = min(alpha, critical_alpha)
    return alpha

Here, if the equation does not have a positive real root, that means for this specific triangle, the step size can be taken arbitrarily large and it will not trigger inversion.

The quadratic equation can be solved as

Implementation 15.3.2 (Solve quadratic equation, utils.py).

def smallest_positive_real_root_quad(a, b, c, tol = 1e-6):
    # return negative value if no positive real root is found
    t = 0
    if abs(a) <= tol:
        if abs(b) <= tol: # f(x) = c > 0 for all x
            t = -1
        else:
            t = -c / b
    else:
        desc = b * b - 4 * a * c
        if desc > 0:
            t = (-b - math.sqrt(desc)) / (2 * a)
            if t < 0:
                t = (-b + math.sqrt(desc)) / (2 * a)
        else: # desv<0 ==> imag, f(x) > 0 for all x > 0
            t = -1
    return t

With scaled coefficients, we simply use a unitless threshold, e.g. \code{1e-6}, to check for degeneracies. If no positive real roots are found, the function simply returns .

Now as we filter the initial step size in addition to non-interpenetration:

Implementation 15.3.3 (Apply filter, time_integrator.py).

        alpha = min(BarrierEnergy.init_step_size(x, n, o, p), NeoHookeanEnergy.init_step_size(x, e, p))  # avoid interpenetration, tunneling, and inversion

and make sure all added data structures and modified functions are reflected in the time integrator, we can finally simulate the compressing square example from Moving Boundary Condition with guaranteed non-inversion (see Figure 15.3.1).

Figure 15.3.1. A square is dropped onto the ground and compressed severely by a ceiling while maintaining inversion-free throughout the simulation. The ground has friction coefficient so that the bottom of the square slides less than the top, where the ceiling has no friction.

Summary

We have successfully implemented an inversion-free 2D elasticity simulation by discretizing the Neo-Hookean model using linear triangle elements.

By maintaining a linearly varying displacement field within each triangle, we can directly calculate a constant deformation gradient for each triangle using both the material and world space coordinates of the vertices. This foundational setup facilitates the computation of the Neo-Hookean energy, as well as its gradient and Hessian with respect to , by applying the chain rule. These calculations are essential for the optimization-based time integration discussed in previous lectures.

To ensure the simulation remains free of both interpenetration and inversion, we adopt a similar strategy as previously described: the initial step size in the line search is determined by solving a quadratic equation for each triangle. This equation calculates a critical step size that reduces the triangle's volume by 90%. The smallest of these critical step sizes across all triangles is then used to initialize the line search, ensuring robustness against both non-interpenetration and non-inversion.

In the upcoming chapter, we will delve into the derivation of the governing equations for hyperelastic solids, providing a detailed explanation of each step to further solidify understanding.

Strong and Weak Forms

The update rules (refer to Equation (1.5.1)) and the corresponding optimization problems (refer to Equation (2.1.1)) utilized in solids simulation are derived by discretizing the conservation laws—our governing equations—from their continuous forms. This chapter will explore the derivation of both the strong and weak forms of these conservation laws. We will then discuss the methods for their temporal and spatial discretizations, which are essential for formulating the discrete problems we aim to solve.

The fundamental governing equations central to our study are the conservation of mass and the conservation of momentum (Newton's Second Law). We will outline these equations below and provide detailed derivations later in this lecture.

Definition 16.1 (Strong Form). Letting be the velocity defined over , the equations are [Gonzalez & Stuart 2008]: where and . Here is the mass density, , is the first Piola-Kirchoff stress, and is the constant gravitational acceleration. Note that , and the mass conservation can also be written as

These equations are initially presented in their strong form. In this lecture, we will also derive the equivalent weak form of the force balance equation (conservation of momentum). The weak form reformulates the conservation law using integral expressions, which are crucial for the subsequent derivation of the temporal and spatial discretizations of the equations using the Finite Element Method.

Conservation of Mass

We can think of the mass density to be naturally defined over as where is the world space counterpart of (the ball of radius surrounding an arbitrary ). This is arguably a natural definition since should be a measurable quantity. Conservation of mass can be expressed as

Now, with a change of variables, we have , so Equation (16.1.1) becomes and so since . Then combining Equations (16.1.2), (16.1.3), and (16.1.4), we can express the conservation of mass as This just says that the mass in (as expressed via an integral of the mass density) should not change with time. This set is associated with a subset of the material at time and as it evolves in the flow, the material will take up more or less space, but there will always be the same amount (mass) of material in the set. Since is arbitrary, it must be true that

Remark 16.1.1 (Lagrangian and Eulerian Views). In simulation methods that discretize and track materials directly based on , conservation of mass is inherently satisfied. For instance, in our Finite Element Method (FEM) simulator, is segmented into triangles, with the mass of each triangle remaining constant regardless of deformation throughout the simulation. This approach is known as the Lagrangian method. In contrast, Eulerian methods discretize and evolve physical quantities based on and often need to specially deal with mass conservation.

Conservation of Momentum

In the continuous setting, forces are categorized into body forces (also known as external forces, such as gravity) and surface forces (or internal forces, typically stress-based, like those arising from elasticity). We define stress-based forces through a traction field, whose existence is assumed. The traction, or force per unit area, is represented by the field and is defined by the equation: where represents the outward-pointing normal direction in the material space. Here, denotes the net force exerted from the material outside on the material inside through their interface. The function quantifies the force per unit area () or length () that material on the side exerts at point on material on the side.

It can be shown that this implies the existence of a stress field (first Piola-Kirchoff stress) with:

Then, by applying Newton's second law on , we can express the conservation of momentum as: for all and .

Applying the divergence theorem, we can transform the boundary integral in Equation (16.2.1) into a volume integral and obtain: for all and .

Definition 16.2.1 (Divergence Theorem for Vectors). For a vector-valued function defined on a closed domain , let be the outward-pointing normal on the boundary of this domain, the following equality holds: This theorem allows us to conveniently transform between boundary and volume integrals.

Here the divergence operator acts on every row vector of independently and results in a column vector: . Since Equation (16.2.2) also holds for arbitrary , we arrive at the strong form of the force balance equation by removing the integration:

Remark 16.2.1 (Momentum Conservation in Solid Simulation). Conservation of momentum is the primary governing equation we use to simulate solids. As discussed previously, both the acceleration, denoted by , and the internal force, expressed as , can be described using world space coordinates . With all other relevant quantities established, we incrementally solve for to get dynamic motions step by step.

Weak Form

First, since the external force term resembles a lot to the time derivative of the momentum on the left-hand side, we will ignore it during the derivation for simplicity. Then, for an arbitrary test function , let's compute the dot product to both sides of Equation (16.2.3) and integrate over to generate the weak form: Here we denote . Without going into details on finite element analysis, we claim that the weak form is sufficiently equivalent to the strong form since Equation (16.3.1) is required to hold for arbitrary , and solving the weak form provides us a solution that is a "good enough" soution to the original problem.

With index notation where means the -th component of vector-valued function , and means , we can rewrite Equation (16.3.1) as If we further omit the summation symbol and let the repetitive subscripts represent summation (this is known as Einstein notation), we obtain Now applying Integration By Parts on the right-hand side, we can rewrite Equation (16.3.3) as

Definition 16.3.1 (Integration By Parts). For a scalar-valued function and a vector-valued function (vector field) , the product rule for divergence states that: Integrating both sides on domain then gives:

Then if we further apply the divergence theorem on the first part of the right-hand side of Equation (16.3.4), we obtain The quantity would be specified as a boundary condition. If we let be the boundary force per unit reference area (traction) with , then we can say that the conservation of momentum implies that This is momentum conservation's weak form written in .

Remark 16.3.1 (Why Weak Form). In finite element method (FEM) for solids, conservation of momentum is formulated in the weak form rather than directly discretizing the strong form due to specific advantages. The strong form requires the displacement field and its derivatives to be continuously differentiable across the entire domain, which is difficult to achieve in practical scenarios involving complex geometries or material discontinuities. On the other hand, the weak form only requires the displacement field itself to be continuous, relaxing the need for continuous derivatives. This makes the weak form more adaptable to irregular mesh geometries and better suited for incorporating boundary conditions and handling interface problems. The weak form's integration-based approach reduces the sensitivity to local irregularities, making it more stable and robust for numerical computation in solid mechanics. Thus, while the strong form provides a direct representation of physical laws, its direct discretization is less practical for the computational demands and complexities typical in FEM analyses.

Summary

In this lecture, we derived the strong forms of the governing equations—conservation of mass and conservation of momentum—focusing on an infinitesimal region within the simulation domain. The conservation of momentum equation was transformed from surface to volume integrals using the divergence theorem.

For Lagrangian simulation methods, such as FEM solid simulation, which discretize and monitor physical quantities based on the material space , the conservation of mass is inherently maintained. We then progressed to deriving the weak form of conservation of momentum. This involved integrating the dot product between the momentum terms and an arbitrary test function. The weak form is effectively equivalent to the strong form because the integral equation must satisfy any arbitrary test function. Techniques such as integration by parts and the application of the divergence theorem were essential in this derivation.

In our next lecture, we will discretize the weak form both temporally and spatially, further refining our approach to solve the discrete problems examined in our case studies.

Discretization of Weak Forms

In this lecture, we will discretize the weak form of the momentum conservation equation (temporarily ignoring body forces) in both space and time to reach the discrete form—a system of equations introduced in the first lecture.

We will begin by focusing on a specific point in time, . From the weak form of the momentum conservation equation (Equation (16.3.6)), we have: for arbitrary , where the superscript denotes quantities measured at . Here:

  • and are specified by the simulation setup,
  • can be calculated from the degrees of freedom via a constitutive law,
  • is the second-order time derivative of , and
  • is an arbitrary vector field.

Discrete Space

To enable numerical evaluation of the integrals in the weak form, the first step is to discretize the smooth vector fields and . This allows them to be represented by a finite set of samples, along with appropriate interpolation functions.

Example 17.1.1 (1D Function Interpolation). In 1D, to approximate a function using three sample points , , (Figure 17.1.1), we can use interpolation functions and form .

Figure 17.1.1. With interpolation functions , , and sample points , , , a function can be approximated as .

Given a set of sample points indexed by or in the simulation domain, we can approximate the test function and the DOF as: where refers to the -th dimension of evaluated at sample point at time , and is the interpolation function at sample point . In this way, we similarly have: Plugging these discretizations into the weak form (Equation (17.1)) and expressing summations with the index notation, we obtain: On the left-hand side, we see that the sample values and are in fact independent of , so we can move them out of the integral and obtain: where is the mass matrix.

Remark 17.1.1 (Mass Matrix Properties). The mass matrix (Equation (17.1.2)) is symmetric and positive semi-definite because it can be expressed as: where . Thus, for any vector , In practice, this mass matrix may be singular. To address this, we typically use a "mass lumping" strategy to approximate the mass matrix with a diagonal and positive definite form. This is achieved by summing each row and defining:

After spatial discretization, the solution of the weak form (Equation (17.1)) is confined to -dimensional function spaces, where represents the number of sample points, assuming all interpolation functions are mutually orthogonal. This means that there could be continuous solutions to the weak form outside of our solution space. In such cases, we can only provide an approximate solution based on the chosen sample points and interpolation functions.

Definition 17.1.1 (Orthogonal Functions). Similar to the orthogonality of two vectors and , defined as , the orthogonality of two functions and is defined as: Just as a basis of vectors can span a finite-dimensional space, orthogonal functions can form an infinite basis for a function space. Conceptually, the integral above is analogous to a vector dot product.

That being said, to generate equations solvable for the unknowns, the arbitrary test function does not need to cover all possibilities to produce an infinite number of equations. Instead, we only need to produce a finite set of equations that spans the entire solution space. Therefore, for traversing all sample points, and , we can assign the test function: to obtain equations: resulting in unknowns and equations, bringing us closer to the discrete form.

The two integrals on the right side of Equation (17.1.3) can be evaluated analytically or using quadrature rules, depending on the specific choice of interpolation functions. We will discuss these in detail in future lectures.

Discrete Time

Discretization in time links to our degrees of freedom (DOF) . In the continuous setting, . Now, let us divide time into small intervals, , as discussed in the first chapter. Using the finite difference formula, we can conveniently approximate in terms of .

For example, with backward Euler: which gives us: where . Applying this relation at the sample points into Equation (17.1.3), we obtain:

Then, by applying mass lumping and zero traction boundary conditions, i.e., , we finally see that Equation (17.2.1) is the -th row of the discrete form of backward Euler time integration in the first lecture: where the elasticity force is obtained by evaluating: which will be discussed in the next chapter.

Summary

In this lecture, we discretized the weak form of momentum conservation in both space and time, arriving at the system of equations for backward Euler time integration introduced in the first lecture.

Spatial Discretization:
For spatial discretization, a finite number of points are sampled within the domain, and their displacements are used as the degrees of freedom (DOF) of the simulation. With the interpolation function associated with each DOF, the displacement at any point in the domain can be approximated, limiting the solution of the weak form to -dimensional function spaces formed by mutually orthogonal interpolation functions, where represents the number of sample points. In this way, the test function can be conveniently assigned to generate equations for solving the unknowns.

Temporal Discretization:
The discretization of time connects the acceleration to the DOF via specific time integration rules. By applying mass lumping and assuming zero traction boundary conditions, we can ultimately derive the discrete form. The integration of interpolation functions will be covered in the next chapter.

In the next lecture, we will discuss boundary conditions and frictional contact in the continuous setting.

Boundary Conditions and Frictional Contact

Until now, we've omitted the Dirichlet boundary conditions and frictional contact in both the strong and weak forms of the governing equations to keep the derivations concise and straightforward. However, as we learned in the Boundary Treatments chapter, this boundary information is crucial for accurately simulating a wide range of solid dynamics.

Incorporating Boundary Conditions

In the weak form we derived (see Equation (16.3.6)), there is a boundary term that describes the force acting on the boundary of the solid from the outside.

If there are no Dirichlet boundary conditions, the entire boundary is handled using Neumann Boundary Conditions, where the boundary force is specified as part of the problem setup. Recall that we discussed the Dirichlet Boundary Condition, where the displacements of the boundary are directly prescribed. In practice, external forces act on the Dirichlet boundaries to ensure their displacements precisely match the prescribed values, and these forces are calculated directly from those displacements.

In a solid simulation problem, boundaries can be either a Dirichlet boundary or a Neumann boundary, which can be described by a more general problem formulation in strong form:

Here and are the Neumann and Dirichlet boundaries respectively, , , and and are given. After we derive the weak form of the momentum conservation (see Equation (18.1.1), first line), the boundary term can be separately considered for Dirichlet and Neumann boundaries:

For Neumann boundaries, since the traction is provided, the above integral can be directly evaluated after discretization. However, for Dirichlet boundaries, remains unknown until we solve the problem. Therefore, a straightforward approach is to introduce the traction at Dirichlet boundaries as unknowns and solve the system that includes both the discretized weak form equations and the Dirichlet boundary conditions.

Remark 18.1.1 (Optimization Form). In the optimization form, the potential energy does not include any Dirichlet boundaries, effectively ignoring the boundary integral in the weak form. This is valid because the Dirichlet boundary conditions will be enforced by the linear equality constraints, and the corresponding discretized weak form equation will be overwritten.

Normal Contact for Non-penetration

To prevent self-interpenetration during simulation, it's essential to enforce a condition ensuring that the deformation map is bijective for any . This bijection is maintained by boundary forces acting on pairs of contacting surface regions, referred to as . We can think of these forces as another set of Neumann boundary conditions that exert extra forces on only when necessary to prevent interpenetration. Thus, we can extend the boundary integral term in the weak form as follows:

where is the original Neumann boundary force specified in the problem setup, and is the normal contact force arising from the bijectivity constraint.

Similar to Dirichlet boundary conditions, can only be determined once we solve the problem. However, enforcing non-interpenetration is more complex than prescribing displacements. Fortunately, we can use the approximate constitutive model of in IPC to represent the contact force as a function of , ensuring non-interpenetration by simply including this additional conservative force.

Remark 18.2.1 (Overlapping Boundaries). Note that here can overlap with both and . For a free (Neumann) boundary contacting a Dirichlet boundary, on the Dirichlet part will also be ignored when enforcing the Dirichlet boundary conditions. However, if two Dirichlet boundaries interpenetrate each other, the problem will have no solution with the bijectivity constraint.

Barrier Potential

As discussed in Distance Barrier for Nonpenetration, the principle of IPC for solid-to-obstacle contact is to use a barrier function to ensure that the signed distance between any nodal degrees of freedom (DOFs) and obstacles remains positive throughout the simulation. To handle self-contact, potentially for codimensional objects, this idea is extended to ensure that the unsigned distance between any boundary points and the boundary remains nonzero throughout the simulation.

Let's consider two colliding regions, and , on the boundary. For any point , we must ensure that the closest distance between and any point on remains nonzero. This can be achieved by using a barrier function to enforce this minimum distance, where the negative gradient of the function provides the contact force. This can be written as

where is the barrier function:

serving as the contact potential energy density. Here, the barrier function approaches infinity as the distance approaches zero, providing an arbitrarily large repulsive force to prevent interpenetration. When the distance is above the threshold and no contact is occurring, no contact forces are exerted. By using the barrier function, the non-smooth contact constraints are approximated by a constitutive model in which the force is conservative, enabling consistent resolution through an optimization-based time integrator.

Remark 18.3.1 (Barrier Density). Compared to Equation (7.2.3), the barrier energy density function here is additionally multiplied by to maintain consistent units after surface integration. Recall from Remark 7.2.1 that the barrier potential can be thought of as an extra thin layer of (-thick) virtual material right outside the boundary of the solids, and is analogous to Young's modulus.

Remark 18.3.2 (Min Operator). When multiple points have the same minimal distance to , the distance barrier of to all these points should be summed up. The min operator is non-smooth, which can still complicate optimization-based time integration. In the next chapter, we will demonstrate how this is approximated as described in Distance Barrier for Nonpenetration.

The case of two colliding regions results in a boundary integral:

where is defined in Equation (18.3.1), and:

However, we have ignored the self-contact of and in this example. Thus, generalizing to arbitrary self-contact for the whole domain, we can keep the single boundary integral term for contact as in Equation (18.2.1) and define the traction more generally as:

where is an infinitesimal circle around with the radius sufficiently small to avoid unnecessary contact forces between a point and its geodesic neighbors.

Remark 18.3.3 (Barrier Force Limits). In Equation (18.3.4), self-contact is ignored for points inside . This is the trade-off for smoothly approximating contact forces, which are discontinuous in a macroscopic view. Similarly, introduces another source of error. However, when and , our model converges to the discontinuous definition. Note that we also need , or there could still be some distance between and that causes the barrier to diverge in the limit.

Finally, we can define the contact potential over the whole boundary as:

Here, the coefficient is used because the barrier energy density of each pair of contacting points will be counted twice in the integral due to symmetry. When computing barrier forces, both occurrences need to be differentiated. Therefore, using the coefficient allows us to match the force definition in Equation (18.3.4). We'll elaborate on this in the next chapter under the discrete weak form.

The contact potential is not required in the weak form but will be useful for optimization-based time integration.

Friction Force

Analogous to Frictional Contact, maximizing the dissipation rate subject to the Coulomb constraint defines friction forces per unit area variationally:

Here, is the relative sliding velocity between and the closest point , is the coefficient of friction, is the normal contact force per unit area, and is the normal direction.

This is equivalent to:

with when , while takes any unit vector orthogonal to when .

In addition, the friction scaling function is also nonsmooth with respect to , since when , and when . These nonsmooth properties can severely hinder or even break the convergence of gradient-based optimization. The mollification of the friction-velocity relationship here follows the same approach as in Frictional Contact.

Summary

We have discussed Neumann and Dirichlet boundary conditions as well as frictional contact in the continuous setting to complete a rigorous problem formulation. Combining everything in strong form, for all :

After deriving the weak form of the momentum equation, the boundary integral term can be separated as follows:

Here, only the Neumann force is given, while all other boundary forces can be determined after solving the coupled system. Fortunately, Dirichlet boundary conditions can be enforced straightforwardly in the optimization framework as linear equality constraints. Frictional contact forces and can both be smoothly approximated as conservative forces with controllable error.

In the next chapter, we will discuss discretizing the weak form using the finite element method (FEM), connecting the derivations in this chapter to the discrete simulation methods.

Linear Finite Elements

From the governing equations in the continuous setting, we derived the discretized weak form system (nd equations) using the backward Euler time integration rule:

In this chapter, we'll start by discussing the shape function in the context of linear finite elements. This exploration will help us understand the underlying implementation detailed in Inversion-Free Elasticity.

We'll focus specifically on simplex finite elements. In 2D, the 2-simplex is a triangle, and we've consistently used triangle meshes throughout this book to discretize the solid domain into a disjoint set of triangular elements.

Definition 19.1 (Simplex).
An n-simplex is a geometric object with vertices that exists in an -dimensional space. It cannot fit in any space of smaller dimension.

Piecewise Linear Displacement Field

For a triangle element with vertices , , and in the solid domain, we can approximate the world space coordinates of an arbitrary point in this element using spatial discretization (see Equation (17.1.1)):

This equation represents a 2D interpolation, extending Experiment Example 17.1.1. Here, we assume that the world space coordinates of any arbitrary point in an element can be interpolated solely from the coordinates of the element's vertices.

Linear finite elements use linear shape functions in Equation (19.1.1), resulting in a piecewise linear (per triangle) displacement field over the entire domain. Before providing the precise expression of in terms of , we'll introduce another parameter space to simplify the derivation.

Let and , we can use them to express the material space coordinates of an arbitrary point in the element as:

Here, is a linear function of . With linear shape functions, the approximation is a linear function of .

Recall that for interpolation, we have to satisfy the conditions . Putting these all together, we can obtain a unique solution:

where we denote as . This indicates that:

Interestingly, with the expression of , , and , we do not necessarily need the precise expression of and for the following derivations to compute each term in Equation (17.2.1).

Remark 19.1.1 (Partition of Unity). The shape functions of FEM satisfy the partition of unity everywhere within each element: One advantage of FEM is that it provides accurate boundary resolution compared to grid or particle-based representations. The boundary nodes of the FEM mesh can be exactly located on the boundary of the continuous domain. The elements are generated inside the domain, connecting the boundary nodes to form the discrete boundary, which converges to the boundary of the continuous domain as resolution increases.
Although particle-based methods can also sample particles on the domain boundary, their spherical shape functions extend beyond the domain, breaking the partition of unity. This creates a "soft" outbound layer of material that makes boundary force computations inaccurate. In contrast, FEM shape functions are nonzero only within each element, where the partition of unity is satisfied everywhere.

Mass Matrix and Lumping

Recall from Discretization of Weak Forms that:

With the solid domain discretized into triangles , we have:

where represents the material space of triangle . Note that for linear triangle elements, since is nonzero only on the incident triangles of node , here we only need to consider triangles with both and being their vertices.

Let us change the integration variable from to , which gives:

For simplicity, let us denote the vertices of this triangle as , , and , and then we have:

where is the area of triangle . Here, and take , , or depending on the vertex indices and . For example, if and correspond to the 2nd and 3rd vertices of triangle , then and . Assuming uniform density, we have:

With mass lumping, , which means:

where contains all the nodes of the mesh, and all off-diagonal entries of are . Similarly, due to the locality of , for each triangle element, only needs to traverse all three triangle vertices:

where denotes the set of triangles incident to node . This result also explains why in Inversion-Free Elasticity when computing the mass for all the nodes, we traverse all triangles, calculate the mass of the triangle and evenly distribute it to the three vertices. With the mass matrix computed, the momentum change and external body force terms including their energy forms are all easy to deal with.

Elasticity Term

For the elasticity term in the discrete weak form system in Equation (19.1), we can write it as the summation of integrals on each triangle in vector form:

Analogously, this summation also only needs to involve the incident triangles of node .

Recall from Strain Energy, to compute the first Piola-Kirchoff stress , we only need the deformation gradient . From Section Kinematics, we know that . Applying the chain rule with the parameter space variables as intermediates, we have:

which is exactly the same as Equation (15.1.1) from our earlier implementation (Section Inversion-Free Elasticity). Here, we also see that with linear finite elements, the deformation gradient field is piecewise constant in , so is .

Then for , depending on the index of in triangle , we can derive it again using parameter space variables as:

This also allows us to see that is constant within any triangle and it is equivalent to since:

Substituting into Equation (19.3.1) we obtain:

which is exactly how nodal elasticity force is computed in Section Inversion-Free Elasticity. This also indicates that the total elasticity potential can be calculated as , which is before spatial discretization.

Remark 19.3.1. [Linear FEM] Linear FEM refers to being a piecewise linear function of , but the elasticity model can still be nonlinear, i.e. can be a nonlinear function of .

Summary

Based on the temporally and spatially discretized weak form, we've explored methods to compute the mass matrix, deformation gradient, and elasticity force under the linear finite element setting, all of which align with our implementation in Section Inversion-Free Elasticity.

With linear finite elements, the world space coordinates are approximated as a piecewise linear function of . This approximation, , is a linear function inside each triangle and is -continuous at the edges. By using two parameters, and , to represent points on each triangle, we can identify the linear shape functions that interpolate the displacements at the triangle vertices and derive the deformation gradient . The mass matrix entries and elasticity terms can then be computed via integration with respect to and .

Piecewise Linear Boundaries

In this lecture, we will continue our discussion on linear finite elements by focusing on boundary conditions and frictional self-contact on piecewise linear boundaries. Specifically, we will examine the computation of the boundary integral term:

We will cover this in the context of Dirichlet and Neumann boundaries, as well as normal and frictional self-contact forces.

Boundary Conditions

Dirichlet

Due to the accurate boundary resolution of the Finite Element Method (FEM), enforcing Dirichlet boundary conditions is straightforward. We only need to constrain the world-space coordinates of the boundary nodes to the prescribed values:

Once these constraints are properly enforced, the Dirichlet boundary integral term can be ignored.

This same mechanism can also be used to prescribe the displacement of any interior nodes. Although this does not directly correspond to any physical effects, it can simplify the simulation setup.

Neumann

For Neumann boundary conditions, we can evaluate the boundary integral term using the parameter space variables and . With triangle mesh discretization, we have:

where is the edge of triangle that is on the Neumann boundary.

For any boundary node in 2D, there will be at most two incident triangles to consider in the integration for linear shape functions. Let's examine the case with two incident triangles. Consider one of the integrals. Without loss of generality, assume (where corresponds to in triangle ), and that is the other node of on the boundary edge. Then, switching the integration variables to gives us:

Here, is simply the edge length . If is constant over the boundary at , we can compute:

Therefore, to add a constant Neumann force to the discrete system, we first calculate the length weight of each boundary node by distributing the length of the boundary edges evenly to their vertices, and then multiply by the traction . If is not constant over the boundary, more complex boundary integral calculations are needed. For a boundary node with only one incident triangle, its length weight comes from its two incident edges within the same triangle.

Remark 20.1.1 (Neumann Boundary Conditions). Here, we observe that the specified traction in standard Neumann boundary conditions is independent of , which simplifies the derivation of the potential energy, even in the continuous setting for varying Neumann forces over the domain: To verify this, we can replace with for spatial discretization. Taking the derivative with respect to gives us the force integral term in the discrete weak form:

Solid-Obstacle Contact

Recall that we used a conservative force model to approximate the contact traction , allowing it to be directly evaluated given the current configuration of the solids. This results in a contact potential:

where is the barrier energy density function, and is an infinitesimal region around where contact is ignored for theoretical soundness.

For normal contact between simulated solids and collision obstacles (ignoring self-contact for now), can be written in a much simpler form Here and are the boundaries of the simulated solids and obstacles respectively, is the point-obstacle distance, and the simplification from two terms to one single term is due to symmetry in the continuous setting. With triangle discretization, Similar to the derivation for Neumann boundaries, for any boundary node , with 2 incident triangles, let us look at one of the integral. Without loss of generality, we can assume ( corresponds to in triangle ), and that is the other node of on the boundary edge. Then, switching the integration variables to gives us Since and are both highly nonlinear functions, we could not obtain a closed-form expression for Equation (20.2.2). If we take the two end points and as quadrature points both with weights , we can approximate the integral as Then, the whole boundary integral can be approximated as assuming that and are the two neighbors of on the boundary. This is now exactly what has been implemented in Filter Line Search.

Remark 20.2.1 (Quadrature Choice for Line Segment). Selecting the two end points () as quadrature points for a line segment integral (Equation (20.2.3)) is not a common design choice. Typically, Gaussian quadrature would use . The advantage of choosing is that it results in fewer quadrature points globally, thus reducing computational costs, as neighboring edges share end points.

To see how connects to the boundary integral (Equation (20.1)) in the discrete weak form, let us take the derivative of the discretized contact potential (Equation (20.2.1)) with respect to : Then we also verified that here.

Self-Contact

With triangle discretization, the boundary of the domain is approximated as a polyline formed by a set of edges. Let us denote this set of boundary edges as , and the barrier potential becomes:

Here, is the set of edges that contain . Completely ignoring these edges is a specific choice of under the current discretization. The term is simply the point-edge distance , which can be calculated as either a point-point distance or a point-line distance depending on the relative positions of the point and the edge.

As we know, the barrier energy density function is already a smooth approximation to the discontinuous normal contact forces that prevent interpenetration between two colliding points. However, when considering self-contact between discrete surfaces (piecewise linear here), the non-smooth operator on point-edge distances is inevitable. This non-smoothness can still pose challenges for optimization time integrators.

To obtain a smooth barrier potential even in the case of piecewise linear boundaries, we first transform the operator to a operator, as the energy density function is a non-ascending function everywhere in the domain. This gives us:

Next, we need to smoothly approximate the operator. A straightforward choice is to use the smooth max function, such as the -norm function:

with sufficiently large. However, the exponent will couple multiple inputs together, increasing the stencil size and making the Hessian less sparse, which will make the simulation more computationally expensive.

Fortunately, due to the local support of , where the contact force only exists for distances smaller than , using is sufficient. With a relatively small , there will only be some redundant contact forces at the interface of boundary elements (Figure 20.3.1).

Figure 20.3.1. In this simple two-edge illustration, the yellow and green regions are only counted once by the summation, but the blue region and the yellow-green overlap are counted twice. If we subtract once the blue region, then for the right-top boundary (convex), it becomes perfect, but for the left-bottom boundary (concave), we can still see some overlap that are counted twice.

Since the overlapping supports of from multiple boundary elements can be clearly identified, it is also possible to subtract the redundant barrier potentials in those regions, as discussed in detail in [Li et al. 2023]. For this book, let us keep it simple by using with the -norm formulation, which is just summation:

Approximating the integral under triangle discretization and picking the end points of each boundary edge as the quadrature points, we obtain the fully discrete form:

Similar to the solid-obstacle contact cases, can be derived by taking the derivative of the whole contact potential with respect to the nodal degrees of freedom (DOFs).

Summary

We have connected the discrete weak form (Equation (19.1)) to the implementations in Filter Line Search for boundary conditions and contact. Additionally, we have derived self-contact between discrete surfaces in 2D, which will be implemented in the next lecture.

The derivations follow a consistent methodology: first, rewrite the global integral as a summation of local element-wise integrals, and then approximate or analytically evaluate the local integrals using certain quadrature rules.

We didn't explicitly discuss friction in this lecture because its force definition in the continuous setting was covered in Boundary Conditions and Frictional Contact. Its integral approximation can be performed similarly to normal contact forces (see Case Study: 2D Frictional Self-Contact for details).

During the derivation, we also observed that the route we have taken from the strong form to the optimization time integration implementation, namely:

is not unique. We can directly write the continuous form of the potential energies and then perform spatial discretization and approximation to obtain the nodal forces. Readers interested in this approach can refer to Lagrangian Mechanics or Hamiltonian Mechanics.

Case Study: 2D Self-Contact*

We have finished connecting linear finite elements to the weak form derivation for elastodynamics and frictional contact. Now, it's time to see how these concepts are implemented in code. In this lecture, we will implement 2D frictionless self-contact based on our Python development of the inversion-free elasticity simulation from Case Study: Inversion-free Elasticity.

The executable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial under the 7_self_contact folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/7_self_contact folder. We will implement frictional self-contact in the next lecture.

Scene Setup and Boundary Element Collection

To begin with, we set up a new scene with two squares falling onto the ground, compressed by the ceiling so that self-contact will occur between these squares.

Implementation 21.1.1 (Simulation setup, simulator.py).

# simulation setup
side_len = 0.45
rho = 1000      # density of square
E = 1e5         # Young's modulus
nu = 0.4        # Poisson's ratio
n_seg = 2       # num of segments per side of the square
h = 0.01        # time step size in s
DBC = [(n_seg + 1) * (n_seg + 1) * 2]   # dirichlet node index
DBC_v = [np.array([0.0, -0.5])]         # dirichlet node velocity
DBC_limit = [np.array([0.0, -0.7])]     # dirichlet node limit position
ground_n = np.array([0.0, 1.0])         # normal of the slope
ground_n /= np.linalg.norm(ground_n)    # normalize ground normal vector just in case
ground_o = np.array([0.0, -1.0])        # a point on the slope  
mu = 0.4        # friction coefficient of the slope

# initialize simulation
[x, e] = square_mesh.generate(side_len, n_seg)       # node positions and triangle node indices of the top square
e = np.append(e, np.array(e) + [len(x)] * 3, axis=0) # add triangle node indices of the bottom square
x = np.append(x, x + [side_len * 0.1, -side_len * 1.1], axis=0) # add node positions of the bottom square

In line 17, we adapt the DOF index of the ceiling from seg+1) to seg+1)2, as we now have two squares. Line 26 generates the first square on the top, while lines 27 and 28 generate the second square on the bottom by creating copies and offsets.

The initial frame, as shown in Figure 21.1.1, is now established. However, without handling self-contact, these two squares cannot interact with each other yet.

Figure 21.1.1. The new scene setup with 2 squares to fall.

To handle contact, we first need to collect all boundary elements. In 2D, this involves identifying the nodes and edges on the boundary where contact forces will be applied to all close but non-incident point-edge pairs. The following function finds all boundary nodes and edges given a triangle mesh:

Implementation 21.1.2 (Collect boundary elements, square_mesh.py).

def find_boundary(e):
    # index all half-edges for fast query
    edge_set = set()
    for i in range(0, len(e)):
        for j in range(0, 3):
            edge_set.add((e[i][j], e[i][(j + 1) % 3]))

    # find boundary points and edges
    bp_set = set()
    be = []
    for eI in edge_set:
        if (eI[1], eI[0]) not in edge_set:
            # if the inverse edge of a half-edge does not exist,
            # then it is a boundary edge
            be.append([eI[0], eI[1]])
            bp_set.add(eI[0])
            bp_set.add(eI[1])
    return [list(bp_set), be]

This function is called in simulator.py, and the boundary elements are then passed to the time integrator for energy, gradient, and Hessian evaluations, as well as line search filtering.

Point-Edge Distance

Next, we calculate the point-edge distance and its derivatives. These will be used to solve for the contact forces. For a node and an edge , their squared distance is defined as

which is the closest squared distance between and any point on .

Remark 21.2.1 (Distance Calculation Optimization). Here, we use the squared unsigned distances for evaluating the contact energies. This approach avoids taking square roots, which can complicate the expression of the derivatives and increase numerical rounding errors during computation. Additionally, unsigned distances can be directly extended for codimensional pairs, such as point-point pairs, which are useful when simulating particle contacts in 2D. They also do not suffer from locking issues, as signed distances do, when there are large displacements.

Fortunately, is a piece-wise smooth function w.r.t. the DOFs: where the smooth expression can be determined by checking whether the node is inside the orthogonal span of the edge. Given these smooth expressions, we can differentiate each of them and obtain the derivatives of the point-edge distance function. The implementations are as follows:

Implementation 21.2.1 (Point-Edge distance calculation (Hessian omitted), PointEdgeDistance.py).

import numpy as np

import distance.PointPointDistance as PP
import distance.PointLineDistance as PL

def val(p, e0, e1):
    e = e1 - e0
    ratio = np.dot(e, p - e0) / np.dot(e, e)
    if ratio < 0:    # point(p)-point(e0) expression
        return PP.val(p, e0)
    elif ratio > 1:  # point(p)-point(e1) expression
        return PP.val(p, e1)
    else:            # point(p)-line(e0e1) expression
        return PL.val(p, e0, e1)

def grad(p, e0, e1):
    e = e1 - e0
    ratio = np.dot(e, p - e0) / np.dot(e, e)
    if ratio < 0:    # point(p)-point(e0) expression
        g_PP = PP.grad(p, e0)
        return np.reshape([g_PP[0:2], g_PP[2:4], np.array([0.0, 0.0])], (1, 6))[0]
    elif ratio > 1:  # point(p)-point(e1) expression
        g_PP = PP.grad(p, e1)
        return np.reshape([g_PP[0:2], np.array([0.0, 0.0]), g_PP[2:4]], (1, 6))[0]
    else:            # point(p)-line(e0e1) expression
        return PL.grad(p, e0, e1)

It can be verified that the point-edge distance function is -continuous everywhere, including at the interfaces between different segments. For the point-point case, we have:

Implementation 21.2.2 (Point-Point distance calculation, PointPointDistance.py).

import numpy as np

def val(p0, p1):
    e = p0 - p1
    return np.dot(e, e)

def grad(p0, p1):
    e = p0 - p1
    return np.reshape([2 * e, -2 * e], (1, 4))[0]

def hess(p0, p1):
    H = np.array([[0.0] * 4] * 4)
    H[0, 0] = H[1, 1] = H[2, 2] = H[3, 3] = 2
    H[0, 2] = H[1, 3] = H[2, 0] = H[3, 1] = -2
    return H

For the point-line case, the distance evaluations can be implemented as follows, and the derivatives can be obtained using symbolic differentiation tools.

Implementation 21.2.3 (Point-Line distance calculation (Hessian omitted), PointLineDistance.py).

import numpy as np

def val(p, e0, e1):
    e = e1 - e0
    numerator = e[1] * p[0] - e[0] * p[1] + e1[0] * e0[1] - e1[1] * e0[0]
    return numerator * numerator / np.dot(e, e)

def grad(p, e0, e1):
    g = np.array([0.0] * 6)
    t13 = -e1[0] + e0[0]
    t14 = -e1[1] + e0[1]
    t23 = 1.0 / (t13 * t13 + t14 * t14)
    t25 = ((e0[0] * e1[1] + -(e0[1] * e1[0])) + t14 * p[0]) + -(t13 * p[1])
    t24 = t23 * t23
    t26 = t25 * t25
    t27 = (e0[0] * 2.0 + -(e1[0] * 2.0)) * t24 * t26
    t26 *= (e0[1] * 2.0 + -(e1[1] * 2.0)) * t24
    g[0] = t14 * t23 * t25 * 2.0
    g[1] = t13 * t23 * t25 * -2.0
    t24 = t23 * t25
    g[2] = -t27 - t24 * (-e1[1] + p[1]) * 2.0
    g[3] = -t26 + t24 * (-e1[0] + p[0]) * 2.0
    g[4] = t27 + t24 * (p[1] - e0[1]) * 2.0
    g[5] = t26 - t24 * (p[0] - e0[0]) * 2.0
    return g

Barrier Energy and Its Derivatives

With the point-edge distance functions implemented, we can traverse all point-edge pairs to assemble the total barrier energy and its derivatives. These will be used to solve for the search direction in the time-stepping optimization.

Since squared distances are used, here we rescale the barrier function to so that still holds. Analogous to elasticity, can be viewed as a strain measure, then the 2nd-order derivative of the energy density (per area) function w.r.t. at would correspond to Young's modulus times thickness , which makes physically meaningful and convenient to set.

Based on Equation (20.3.1), we can derive the gradient and Hessian of the barrier potential as where and we omitted the superscripts and subscripts for the squared point-edge distance functions ( denotes here).

The energy, gradient, and Hessian of the barrier contact potential are implemented as follows:

Implementation 21.3.1 (Barrier energy computation, BarrierEnergy.py).

    # self-contact
    dhat_sqr = dhat * dhat
    for xI in bp:
        for eI in be:
            if xI != eI[0] and xI != eI[1]: # do not consider a point and its incident edge
                d_sqr = PE.val(x[xI], x[eI[0]], x[eI[1]])
                if d_sqr < dhat_sqr:
                    s = d_sqr / dhat_sqr
                    # since d_sqr is used, need to divide by 8 not 2 here for consistency to linear elasticity:
                    sum += 0.5 * contact_area[xI] * dhat * kappa / 8 * (s - 1) * math.log(s)

Implementation 21.3.2 (Barrier energy gradient computation, BarrierEnergy.py).

    # self-contact
    dhat_sqr = dhat * dhat
    for xI in bp:
        for eI in be:
            if xI != eI[0] and xI != eI[1]: # do not consider a point and its incident edge
                d_sqr = PE.val(x[xI], x[eI[0]], x[eI[1]])
                if d_sqr < dhat_sqr:
                    s = d_sqr / dhat_sqr
                    # since d_sqr is used, need to divide by 8 not 2 here for consistency to linear elasticity:
                    local_grad = 0.5 * contact_area[xI] * dhat * (kappa / 8 * (math.log(s) / dhat_sqr + (s - 1) / d_sqr)) * PE.grad(x[xI], x[eI[0]], x[eI[1]])
                    g[xI] += local_grad[0:2]
                    g[eI[0]] += local_grad[2:4]
                    g[eI[1]] += local_grad[4:6]

Implementation 21.3.3 (Barrier energy Hessian computation, BarrierEnergy.py).

    # self-contact
    dhat_sqr = dhat * dhat
    for xI in bp:
        for eI in be:
            if xI != eI[0] and xI != eI[1]: # do not consider a point and its incident edge
                d_sqr = PE.val(x[xI], x[eI[0]], x[eI[1]])
                if d_sqr < dhat_sqr:
                    d_sqr_grad = PE.grad(x[xI], x[eI[0]], x[eI[1]])
                    s = d_sqr / dhat_sqr
                    # since d_sqr is used, need to divide by 8 not 2 here for consistency to linear elasticity:
                    local_hess = 0.5 * contact_area[xI] * dhat * utils.make_PSD(kappa / (8 * d_sqr * d_sqr * dhat_sqr) * (d_sqr + dhat_sqr) * np.outer(d_sqr_grad, d_sqr_grad) \
                        + (kappa / 8 * (math.log(s) / dhat_sqr + (s - 1) / d_sqr)) * PE.hess(x[xI], x[eI[0]], x[eI[1]]))
                    index = [xI, eI[0], eI[1]]
                    for nI in range(0, 3):
                        for nJ in range(0, 3):
                            for c in range(0, 2):
                                for r in range(0, 2):
                                    IJV[0].append(index[nI] * 2 + r)
                                    IJV[1].append(index[nJ] * 2 + c)
                                    IJV[2] = np.append(IJV[2], local_hess[nI * 2 + r, nJ * 2 + c])

Continuous Collision Detection

Now, we have all the ingredients to solve for the search direction in a simulation with self-contact. After obtaining the search direction, we perform line search filtering for the point-edge pairs.

Implementation 21.4.1 (Line search filtering, BarrierEnergy.py).

    # self-contact
    for xI in bp:
        for eI in be:
            if xI != eI[0] and xI != eI[1]: # do not consider a point and its incident edge
                if CCD.bbox_overlap(x[xI], x[eI[0]], x[eI[1]], p[xI], p[eI[0]], p[eI[1]], alpha):
                    toc = CCD.narrow_phase_CCD(x[xI], x[eI[0]], x[eI[1]], p[xI], p[eI[0]], p[eI[1]], alpha)
                    if alpha > toc:
                        alpha = toc

Here, we perform an overlap check on the bounding boxes of the spans of the point and edge first to narrow down the number of point-edge pairs for which we need to compute the time of impact:

Implementation 21.4.2 (Bounding box overlap check, CCD.py).

from copy import deepcopy
import numpy as np
import math

import distance.PointEdgeDistance as PE

# check whether the bounding box of the trajectory of the point and the edge overlap
def bbox_overlap(p, e0, e1, dp, de0, de1, toc_upperbound):
    max_p = np.maximum(p, p + toc_upperbound * dp) # point trajectory bbox top-right
    min_p = np.minimum(p, p + toc_upperbound * dp) # point trajectory bbox bottom-left
    max_e = np.maximum(np.maximum(e0, e0 + toc_upperbound * de0), np.maximum(e1, e1 + toc_upperbound * de1)) # edge trajectory bbox top-right
    min_e = np.minimum(np.minimum(e0, e0 + toc_upperbound * de0), np.minimum(e1, e1 + toc_upperbound * de1)) # edge trajectory bbox bottom-left
    if np.any(np.greater(min_p, max_e)) or np.any(np.greater(min_e, max_p)):
        return False
    else:
        return True

To calculate a sufficiently large conservative estimation of the time of impact (TOI), we cannot directly calculate the TOI and take a proportion of it as we did for point-ground contact in Filter Line Search. Directly calculating the TOI for contact primitive pairs requires solving quadratic or cubic root-finding problems in 2D and 3D, which are prone to numerical errors, especially when distances are tiny and configurations are numerically degenerate (e.g., nearly parallel edge-edge pairs in 3D).

Thus, we implement the additive CCD method (ACCD) [Li et al. 2021], which iteratively moves the contact pairs along the search direction until the minimum separation distance is reached, to robustly estimate the TOI.

Taking a point-edge pair as an example, the key insight of ACCD is that, given the current positions , , and search directions , , , its TOI can be calculated as

assuming is the point on the edge that will first collide with. The issue is that we do not know a priori. However, we can derive a lower bound for as

By taking a step with this lower bound , we are guaranteed to have no interpenetration for this pair. However, although straightforward to compute, can be much smaller than . Therefore, we iteratively calculate and advance a copy of the participating nodes by this amount, accumulating all to monotonically improve the estimate of until the point-edge pair reaches a distance smaller than the minimum separation, e.g., the original distance. The implementation is as follows, where we first remove the shared components of the search directions so that they have smaller magnitudes to achieve earlier termination of the algorithm.

Implementation 21.4.3 (ACCD method implementation, CCD.py).

# compute the first "time" of contact, or toc,
# return the computed toc only if it is smaller than the previously computed toc_upperbound
def narrow_phase_CCD(_p, _e0, _e1, _dp, _de0, _de1, toc_upperbound):
    p = deepcopy(_p)
    e0 = deepcopy(_e0)
    e1 = deepcopy(_e1)
    dp = deepcopy(_dp)
    de0 = deepcopy(_de0)
    de1 = deepcopy(_de1)

    # use relative displacement for faster convergence
    mov = (dp + de0 + de1) / 3 
    de0 -= mov
    de1 -= mov
    dp -= mov
    maxDispMag = np.linalg.norm(dp) + math.sqrt(max(np.dot(de0, de0), np.dot(de1, de1)))
    if maxDispMag == 0:
        return toc_upperbound

    eta = 0.1 # calculate the toc that first brings the distance to 0.1x the current distance
    dist2_cur = PE.val(p, e0, e1)
    dist_cur = math.sqrt(dist2_cur)
    gap = eta * dist_cur
    # iteratively move the point and edge towards each other and
    # grow the toc estimate without numerical errors
    toc = 0
    while True:
        tocLowerBound = (1 - eta) * dist_cur / maxDispMag

        p += tocLowerBound * dp
        e0 += tocLowerBound * de0
        e1 += tocLowerBound * de1
        dist2_cur = PE.val(p, e0, e1)
        dist_cur = math.sqrt(dist2_cur)
        if toc != 0 and dist_cur < gap:
            break

        toc += tocLowerBound
        if toc > toc_upperbound:
            return toc_upperbound

    return toc

The final simulation results are demonstrated in Figure 21.4.1.

Figure 21.4.1. Two squares dropped onto the ground and compressed by a ceiling. The ground has friction coefficient but there is no friction between the squares so that the top square slides down to the ground without significantly changing the position of the bottom one.

Summary

We have implemented frictionless self-contact with guaranteed non-intersection for 2D FEM simulations by discretizing barrier energies onto the non-incident point-edge pairs on the boundary.

To compute the barrier energies, we used squared point-edge distances to avoid potential numerical issues. The point-edge distance is a piecewise smooth function with closed-form expressions depending on the relative positions of the point and the edge, and the overall function is -continuous everywhere. The derivatives of the function can be conveniently obtained by applying symbolic differentiation to each expression.

For line search filtering, instead of directly computing the time of impact (TOI) which is prone to numerical issues, we implemented the additive CCD method (ACCD) to obtain a sufficiently large and conservative estimate of TOI. ACCD is an iterative method that accumulates lower bounds of TOI while progressively advancing the nodes along the search direction. Before running ACCD, we perform overlap checks on the bounding boxes of the point's and edge's spans to quickly filter out non-colliding pairs.

In later lectures, we will see that for large-scale scenes in 3D, efficient spatial indexing strategies such as spatial hashing and bounding box hierarchies (BVH) will be needed to significantly reduce the expensive spatial search costs.

In the next lecture, we will implement frictional self-contact based on what we have just developed.

2D Frictional Self-Contact*

In this lecture, we implement 2D friction based on our 2D self-contact implementation in Case Study: 2D Self-Contact. The executable Python project for this section can be found at https://github.com/phys-sim-book/solid-sim-tutorial.

For simplicity, we will focus on implementing a semi-implicit version of friction. This means the normal force magnitude and the tangent operator will be discretized to the last time step, and we solve the optimization once per time step without further fixed-point iterations that converge to solutions with fully-implicit friction (Frictional Contact) under the 8_self_friction folder. MUDA GPU implementations can be found at https://github.com/phys-sim-book/solid-sim-tutorial-gpu under the simulators/8_self_friction folder. Combined with the smoothly approximated static-dynamic friction transition in IPC, implementing friction into an optimization time integration framework is as straightforward as adding an extra potential energy.

Discretization and Approximation

From Equation (18.4.2), the friction force per unit area is defined as

where is the friction coefficient, is the normal contact force, and is the relative sliding velocity. Here when , while takes any unit vector orthogonal to the normal when . Additionally, the friction scaling function is also nonsmooth with respect to , as when , and when .

It is important to note that without temporal discretization, there is no potential energy for friction. However, similar to Frictional Contact, once we discretize the normal force magnitude and the tangent operator to the last time step and smoothly approximate the friction scaling function , the friction force at the -th time step becomes integrable with respect to , and we obtain

Here, is the approximate relative sliding velocity, where and are the normal direction and the point in contact with in the last time step, , and

Therefore, considering self-contact, the approximate friction potential over the entire boundary can be written as

where the scaling comes from double counting the friction between each pair of contact points in the integral (similar to the normal contact forces in Boundary Conditions and Frictional Contact).

After discretizing the boundary curves as polylines and approximating the max operator in the normal contact force component using summations (Piecewise Linear Boundaries), we similarly obtain the spatially discretized friction potential:

Here, is the point-edge distance between and edge in the last time step, and is the approximate relative sliding velocity of the point-edge pair with contact normal and the closest point discretized to the last time step (see next section for details).

If we choose boundary nodes as quadrature points to approximate the integral, we finally obtain our discrete friction potential:

where is the integration weight. Denoting and , the expression of agrees with the discrete form of Equation (9.2.1) we directly derived, except that here traverses all non-incident point-edge pairs on the boundary.

Based on this discrete form of the smoothed semi-implicit friction potential, we now need to determine how to calculate and for point-edge pairs, implement the computation of the value, gradient, and Hessian of , and then incorporate them into the optimization.

Precomputing Normal and Tangent Information

To make the temporally discretized friction force integrable, we must explicitly discretize certain normal and tangent information. This information only needs to be calculated once at the beginning of each time step, as it will remain constant during each optimization.

First, we need to calculate for each point-edge pair using . Recall that we used squared distances as input for the barrier functions, so should be calculated using the chain rule as follows:

According to the scaled barrier function taking squared distance as input (Equation (21.3.1)), we can derive

Remark 22.2.1. The set of point-edge pairs for friction in our semi-implicit friction setting is fixed in each time step and is different from the set of normal contact pairs. The set for friction only contains those pairs with , and this does not change with the optimization variable in the current time step.

Now for the tangent information, the key is to keep the normal and the barycentric coordinate of the closest point on the edge constant. For the -th point-edge pair, if we denote the node indices for the point and edge as , , and , then we can write the relative sliding velocity as

where is the barycentric coordinate and is the normal of the edge. Here we see that and are both dependent on , so directly integrating is nontrivial. By calculating and using , we obtain the semi-implicit relative sliding velocity

and now only the velocities are dependent on , which makes both integration and differentiation straightforward. If we denote , we obtain

Code

Next, let's look at the code. Implementation 22.2.1 calculates the barycentric coordinate of the closest point and the normal given point-edge nodal positions. The idea is to orthogonally project onto the edge.

Implementation 22.2.1 (Calculating contact point and normal, PointEdgeDistance.py).

# compute normal and the parameterization of the closest point on the edge
def tangent(p, e0, e1):
    e = e1 - e0
    ratio = np.dot(e, p - e0) / np.dot(e, e)
    if ratio < 0:    # point(p)-point(e0) expression
        n = p - e0
    elif ratio > 1:  # point(p)-point(e1) expression
        n = p - e1
    else:            # point(p)-line(e0e1) expression
        n = p - ((1 - ratio) * e0 + ratio * e1)
    return [n / np.linalg.norm(n), ratio]

Then, Implementation 22.2.2 traverses all non-incident point-edge pairs with a distance smaller than , calculates , and calls the above function to calculate and .

As in Frictional Contact, these lines of code are executed at the beginning of each time step in time_integrator.py, and the information for each friction pair is stored and passed to the energy, gradient, and Hessian computation functions that we will discuss next.

Implementation 22.2.2 (Semi-implicit friction precomputation, BarrierEnergy.py).

    # self-contact
    mu_lambda_self = []
    dhat_sqr = dhat * dhat
    for xI in bp:
        for eI in be:
            if xI != eI[0] and xI != eI[1]: # do not consider a point and its incident edge
                d_sqr = PE.val(x[xI], x[eI[0]], x[eI[1]])
                if d_sqr < dhat_sqr:
                    s = d_sqr / dhat_sqr
                    # since d_sqr is used, need to divide by 8 not 2 here for consistency to linear elasticity
                    # also, lambda = -\partial b / \partial d = -(\partial b / \partial d^2) * (\partial d^2 / \partial d)
                    mu_lam = mu * -0.5 * contact_area[xI] * dhat * (kappa / 8 * (math.log(s) / dhat_sqr + (s - 1) / d_sqr)) * 2 * math.sqrt(d_sqr)
                    [n, r] = PE.tangent(x[xI], x[eI[0]], x[eI[1]]) # normal and closest point parameterization on the edge
                    mu_lambda_self.append([xI, eI[0], eI[1], mu_lam, n, r])

Friction Energy and Its Derivatives

With , , and precomputed for each friction point-edge pair, we can now conveniently compute the energy (Implementation 22.3.1), gradient (Implementation 22.3.2), and Hessian (Implementation 22.3.3) of the friction potential and add them into the optimization.

Implementation 22.3.1 (Friction energy value, FrictionEnergy.py).

    # self-contact:
    for i in range(0, len(mu_lambda_self)):
        [xI, eI0, eI1, mu_lam, n, r] = mu_lambda_self[i]
        T = np.identity(2) - np.outer(n, n)
        rel_v = v[xI] - ((1 - r) * v[eI0] + r * v[eI1])
        vbar = np.transpose(T).dot(rel_v)
        sum += mu_lam * f0(np.linalg.norm(vbar), epsv, hhat)

When computing the gradient and Hessian, we used the relative velocity as an intermediate variable to make the implementation more organized. This approach is given by: where the derivatives of with respect to have exactly the same forms as in Frictional Contact.

Implementation 22.3.2 (Friction energy gradient, FrictionEnergy.py).

    # self-contact:
    for i in range(0, len(mu_lambda_self)):
        [xI, eI0, eI1, mu_lam, n, r] = mu_lambda_self[i]
        T = np.identity(2) - np.outer(n, n)
        rel_v = v[xI] - ((1 - r) * v[eI0] + r * v[eI1])
        vbar = np.transpose(T).dot(rel_v)
        g_rel_v = mu_lam * f1_div_vbarnorm(np.linalg.norm(vbar), epsv) * T.dot(vbar)
        g[xI] += g_rel_v
        g[eI0] += g_rel_v * -(1 - r)
        g[eI1] += g_rel_v * -r

Implementation 22.3.3 (Friction energy Hessian, FrictionEnergy.py).

    # self-contact:
    for i in range(0, len(mu_lambda_self)):
        [xI, eI0, eI1, mu_lam, n, r] = mu_lambda_self[i]
        T = np.identity(2) - np.outer(n, n)
        rel_v = v[xI] - ((1 - r) * v[eI0] + r * v[eI1])
        vbar = np.transpose(T).dot(rel_v)
        vbarnorm = np.linalg.norm(vbar)
        inner_term = f1_div_vbarnorm(vbarnorm, epsv) * np.identity(2)
        if vbarnorm != 0:
            inner_term += f_hess_term(vbarnorm, epsv) / vbarnorm * np.outer(vbar, vbar)
        hess_rel_v = mu_lam * T.dot(utils.make_PSD(inner_term)).dot(np.transpose(T)) / hhat
        index = [xI, eI0, eI1]
        d_rel_v_dv = [1, -(1 - r), -r]
        for nI in range(0, 3):
            for nJ in range(0, 3):
                for c in range(0, 2):
                    for r in range(0, 2):
                        IJV[0].append(index[nI] * 2 + r)
                        IJV[1].append(index[nJ] * 2 + c)
                        IJV[2] = np.append(IJV[2], d_rel_v_dv[nI] * d_rel_v_dv[nJ] * hess_rel_v[r, c])

After these implementations, we can finally run our compressing squares example with frictional self-contact (see: Figure 22.3.1). From the figure, we observe that once the two squares touch, the large friction between them and the ground restricts any sliding. This causes the squares to rotate counter-clockwise as they are compressed by the ceiling.

Figure 22.3.1. Two squares dropped onto the ground and compressed by a ceiling. The friction coefficient is between any contacting surfaces, which restricts any sliding here in this scene and results in counter-clockwise rotations of the two squares under compression. As their interface becomes nearly vertical, the squares are finally detached.

Summary

We implemented semi-implicit friction in 2D based on squared unsigned distances of point-edge pairs and incorporated it into the time-stepping optimization.

We began by making the friction force integrable in the continuous setting through semi-implicit temporal discretization and a smooth approximation of the dynamic-static friction transition. The spatial discretization of the approximate friction potential follows a similar approach to the barrier potential.

Next, we examined the computation of the normal force magnitude , normal direction , and barycentric coordinate of the closest point for point-edge pairs. These values are calculated at the beginning of each time step and remain constant during the optimization. It is important to note that the set of point-edge pairs for friction is also constant per optimization and differs from the set used for the barrier.

Finally, we implemented the computation of the discrete friction potential and its derivatives. We used relative velocities as intermediate variables and applied the chain rule to organize the calculations.

Up to now, we have covered both the theoretical and practical aspects of a 2D solid simulator with inversion-free elastodynamics and interpenetration-free frictional self-contact. Next, we will explore the additional steps needed to extend these concepts to 3D!

3D Elastodynamics

To extend our 2D solid simulator (2D Frictional Self-Contact) to 3D, we can use -simplex tetrahedral elements to discretize the 3D solid domains. In this approach, the surface of the solid is represented as a triangle mesh, which is a common method in computer graphics for representing 3D geometries. Additionally, we need to sample vertices in the interior of the solid to form the tetrahedral elements required for discretizing the inertia and elasticity energies.

Kinematics

Similar to 2D triangle elements, we use with to express the material space coordinates of an arbitrary point in the tetrahedral element defined by vertices and as follows: Here, is a linear function of . Using linear shape functions, the approximate world-space coordinate is also a linear function of : where is denoted as . This implies that the shape functions are:

Mass Matrix

Recall that the mass matrix can be calculated as where represents the material space of tetrahedron . Changing the integration variable from to results in

For element with vertices , , , and , where is the volume of tetrahedron .

Here, we will omit the detailed derivations of each entry in the consistent mass matrix. Assuming uniform density , for the lumped mass matrix, where denotes the set of tetrahedra incident to node . In other words, the mass of each tetrahedron is evenly distributed among its 4 nodes, which is intuitively analogous to the 2D case.

Elasticity

For elasticity, similar to the 2D case, the deformation gradient is also constant within each tetrahedron, and we can compute it as For force and Hessian computation, the required can be computed using and similarly With , the computation of strain energy , stress and stress derivative can all be found in Strain Energy and Stress and Its Derivatives, and the computation of forces and Hessian matrices follow the same spirit as in 2D.

To guarantee non-inversion of the tetrahedral elements during the simulation, the critical step size that first brings the volume of any tetrahedra to can be obtained by solving a 1D equation per tetrahedron and then take the minimum of the solved step sizes. Here is the search direction of node , and in 3D, this is equivalent to with and , . Expanding Equation (23.3.1), we obtain the following cubic equation for :

This cubic equation can sometimes degenerate into a quadratic or linear equation, particularly when node movements do not substantially alter the tetrahedron's volume. To address potential numerical instability, we scale the equation terms based on the constant term coefficient:

ensuring that magnitude checks can be safely performed with standard thresholds (e.g., ).

Practically, we also ensure some safety margin by solving for that reduces the volume of any tetrahedron by 80%, modifying the constant term coefficient in Equation (23.3.2) from to . If no positive real roots are found, the step size can be considered safe, and inversion will not occur. Here is the C++ code snippet for solving this scaled cubic equation:

Implementation 23.3.1 (Cubic Equation Solver).

double getSmallestPositiveRealRoot_cubic(double a, double b, double c, double d,
    double tol)
{
    // return negative value if no positive real root is found
    double t = -1;

    if (abs(a) <= tol)
        t = getSmallestPositiveRealRoot_quad(b, c, d, tol); // covered in the 2D case
    else {
        complex<double> i(0, 1);
        complex<double> delta0(b * b - 3 * a * c, 0);
        complex<double> delta1(2 * b * b * b - 9 * a * b * c + 27 * a * a * d, 0);
        complex<double> C = pow((delta1 + sqrt(delta1 * delta1 - 4.0 * delta0 * delta0 * delta0)) / 2.0, 1.0 / 3.0);
        if (std::abs(C) == 0.0) // a corner case
            C = pow((delta1 - sqrt(delta1 * delta1 - 4.0 * delta0 * delta0 * delta0)) / 2.0, 1.0 / 3.0);

        complex<double> u2 = (-1.0 + sqrt(3.0) * i) / 2.0;
        complex<double> u3 = (-1.0 - sqrt(3.0) * i) / 2.0;

        complex<double> t1 = (b + C + delta0 / C) / (-3.0 * a);
        complex<double> t2 = (b + u2 * C + delta0 / (u2 * C)) / (-3.0 * a);
        complex<double> t3 = (b + u3 * C + delta0 / (u3 * C)) / (-3.0 * a);

        if ((abs(imag(t1)) < tol) && (real(t1) > 0))
            t = real(t1);
        if ((abs(imag(t2)) < tol) && (real(t2) > 0) && ((real(t2) < t) || (t < 0)))
            t = real(t2);
        if ((abs(imag(t3)) < tol) && (real(t3) > 0) && ((real(t3) < t) || (t < 0)))
            t = real(t3);
    }
    return t;
}

Summary

In this section, we delve into the process of extending our solid simulator to accommodate 3D elastodynamic simulation.

This enhancement involves discretizing the solid domain using 3-simplex tetrahedral elements. Consequently, the kinematics, mass matrix, and elasticity energies adopt the same approach as in 2D, but now incorporate an additional dimension for the per-element parameter space, integration, and deformation gradient.

To maintain inversion-free elements, line search filtering operates similarly, though it now entails solving cubic equations for each element.

In the following section, we will explore the extension of the frictional contact component to 3D scenarios.

3D Frictional Self-Contact

In 3D, the contact between the solid domain boundaries represented as triangle meshes can be reduced to point-triangle and edge-edge contacts. Intuitively, the point-edge contact pairs in 2D extend directly to 3D as point-triangle pairs. However, even if we prevent all point-triangle interpenetrations in 3D, the triangle meshes can still penetrate each other. This necessitates accounting for edge-edge pairs, especially when the resolution of the mesh is not very high.

Barrier and Distances

With triangle mesh discretization, the barrier potential in the continuous settings (Equation (18.3.5)) can be approximated as where is the set of all surface triangles, is the set of all surface triangles that hold point , and is the point-triangle distance. Further approximating the max operator with summations and use mesh surface nodes as quadrature points, we have where is the integration weight and is the area of node 's incident surface triangle .

Now, getting back to the second line of Equation (24.1.1), if we only use points on the edges to approximate the minimum distance, we obtain Then if we choose a special quadrature point per surface edge and approximate the max operators with summations, we get where is the integration weight and is the area of 's incident surface triangle . Next, if we always select to be the closest point to on , we will get where is the set of all the surface edge neighbors of plus itself. For the summation over all surface edges in Equation (24.1.3), if we only account for with or the other way around, then the coefficient can be omitted.

Now we have two kinds of discretizations for the 3D barrier potential energy. To use them together in practice, we can take advantage of a linear combination of them, and the coefficient could usually be set to .

For point-triangle and edge-edge distances, they are also both small optimization problems with analytical solutions, which can be represented as piecewise smooth functions like the 2D Point-Edge distance in Equation (21.2.1). For example, in the point-triangle case, the expression can be determined by checking which region the projection of the point onto the triangle plane is located.

Definition 24.1.1 (3D Point-Triangle Distance). The distance between point and triangle with vertices , , and can be defined as

Definition 24.1.2 (3D Edge-Edge Distance). The distance between edge with end nodes and and edge with end nodes and can be defined as

Remark 24.1.1 (Smoothness of 3D Distance Functions). Note that the point-triangle distance is at least continuous everywhere. This means that even when the projected point is located on the borders of the piecewise function, the distance gradient still exists and is continuous. However, for edge-edge distance, when the edges are parallel, the distance function is only continuous, as the gradient of the expressions in adjacent regions do not agree. To address this issue, IPC [Li et al. 2020] proposed multiplying a mollifier to the edge-edge barrier energy density function to make the potential continuous everywhere. This mollifier smoothly decreases to zero when the edges are parallel. This ensures that gradient-based optimization methods can still be applied efficiently to solve the problem.

Collision Detection

Collision detection in 3D can be significantly more computationally intensive than in 2D due to the larger number of surface primitives involved. Thankfully, spatial data structures like spatial hashing and bounding volume hierarchies (BVH) help efficiently reduce the number of candidate primitive pairs, making continuous collision detection (CCD) more manageable.

Spatial Hashing

The core idea of spatial hashing is to partition the space into a uniform grid and assign each grid cell an array to store the indices of primitives whose bounding boxes intersect with that cell. To find the nearby primitives of a given primitive (e.g., a point), we identify the grid cells intersecting with 's bounding box and retrieve the primitive indices stored in these cells. This approach ensures that only nearby primitives are checked for collisions using CCD, eliminating the need for a nested loop to examine all primitive pairs.

Bounding Volume Hierarchies (BVH)

BVH is another effective method for broad-phase collision detection. It organizes primitives into a hierarchy of bounding volumes, allowing for efficient pruning of the search space when detecting potential collisions.

ACCD Method

The ACCD (Adaptive Continuous Collision Detection) method, as discussed in Continuous Collision Detection, is applicable in 3D. In this context, the distance calculations need to be adapted for point-triangle and edge-edge pairs.

Broad Phase Collision Detection

For computing the barrier potential energy, gradient, and Hessian, it is faster and essential to first gather a set of nearby candidate primitive pairs. Then, we compute their distances to determine if they are active (i.e., within a distance ). This filtering process is part of the broad-phase collision detection and can be efficiently implemented using spatial hashing or BVH.

By employing these spatial data structures, we significantly reduce the computational load, focusing our detailed collision checks on a manageable subset of nearby primitives.

Friction

3D friction is quite similar to its 2D counterpart, with the primary difference being the types of contact pairs involved. In 3D, these contact pairs are point-triangle and edge-edge pairs. Consequently, the barycentric coordinates of the closest points are now two-dimensional, represented by the optimal values of and in the definitions provided in Definition 24.1.1 and Definition 24.1.2.

In practice, this means that while the principles of friction remain the same, the specific calculations adjust to account for the geometry of the contact pairs in 3D space.

Summary

In this section, we discussed the main technical details of implementing a 3D contact handling method based on Implicit Contact Prediction (IPC).

In 3D, both distance and friction basis computations become more complex. These computations rely on point-triangle and edge-edge primitive pairs, similar to the point-edge pairs used in 2D.

For edge-edge distances, which are only -continuous, an additional mollification function that smoothly decreases to zero is necessary. This function is multiplied with the barrier energy density function to achieve -continuity, enabling the use of efficient gradient-based optimization methods.

Due to the significantly larger number of primitive pairs in 3D, spatial data structures like spatial hashing or bounding volume hierarchies (BVH) are often used in the broad phase to filter candidates before computing distances or performing CCD.

Bibliography

[Jiang et al. 2016] - Jiang, Chenfanfu and Schroeder, Craig and Teran, Joseph and Stomakhin, Alexey and Selle, Andrew - The Material Point Method for Simulating Continuum Materials. - 2016. -

Summary/Abstract

N/A

[Chen et al. 2022] - Chen, Yunuo and Li, Minchen and Lan, Lei and Su, Hao and Yang, Yin and Jiang, Chenfanfu - A unified newton barrier method for multibody dynamics. - 2022. -

Summary/Abstract

N/A

[Li et al. 2020] - Li, Minchen and Ferguson, Zachary and Schneider, Teseo and Langlois, Timothy R and Zorin, Denis and Panozzo, Daniele and Jiang, Chenfanfu and Kaufman, Danny M - Incremental potential contact: intersection-and inversion-free, large-deformation dynamics.. - 2020. -

Summary/Abstract

N/A

[Stomakhin et al. 2012] - Stomakhin, Alexey and Howes, Russell and Schroeder, Craig A and Teran, Joseph M - Energetically Consistent Invertible Elasticity.. - 2012. -

Summary/Abstract

N/A

[Smith et al. 2018] - Smith, Breannan and Goes, Fernando De and Kim, Theodore - Stable neo-hookean flesh simulation. - 2018. -

Summary/Abstract

N/A

[Schroeder 2022] - Schroeder, Craig - Practical course on computing derivatives in code. - 2022. -

Summary/Abstract

N/A

[Irving et al. 2004] - Irving, Geoffrey and Teran, Joseph and Fedkiw, Ronald - Invertible finite elements for robust simulation of large deformation. - 2004. -

Summary/Abstract

N/A

[Gonzalez & Stuart 2008] - Gonzalez, Oscar and Stuart, Andrew M - A first course in continuum mechanics. - 2008. -

Summary/Abstract

N/A

[Sifakis & Barbic 2022] - Sifakis, Eftychios and Barbic, Jernej - Finite element method simulation of 3d deformable solids. - 2022. -

Summary/Abstract

N/A

[Xu et al. 2015] - Xu, Hongyi and Sin, Funshing and Zhu, Yufeng and Barbic, Jernej - Nonlinear material design using principal stretches. - 2015. -

Summary/Abstract

N/A

[Li et al. 2023] - Li, Minchen and Ferguson, Zachary and Schneider, Teseo and Langlois, Timothy and Zorin, Denis and Panozzo, Daniele and Jiang, Chenfanfu and Kaufman, Danny M - Convergent Incremental Potential Contact. - 2023. -

Summary/Abstract

N/A

[Li et al. 2021] - Li, Minchen and Kaufman, Danny M and Jiang, Chenfanfu - Codimensional incremental potential contact. - 2021. -

Summary/Abstract

N/A