Skip to content

Commit 057eac8

Browse files
Add validation of hinge model with random quad design
1 parent 26a6399 commit 057eac8

File tree

2 files changed

+540
-1
lines changed

2 files changed

+540
-1
lines changed

exp/hinge_characterization_3dp_pla_shims/random_quads_8x8_compression.ipynb

Lines changed: 273 additions & 0 deletions
Large diffs are not rendered by default.

problems/hinge_characterization.py

Lines changed: 267 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from difflexmm.dynamics import setup_dynamic_solver
1111
from difflexmm.energy import build_contact_energy, build_strain_energy, combine_block_energies, ligament_energy, ligament_energy_linearized
12-
from difflexmm.geometry import RotatedSquareGeometry
12+
from difflexmm.geometry import QuadGeometry, RotatedSquareGeometry
1313
from difflexmm.utils import ContactParams, ControlParams, GeometricalParams, LigamentParams, MechanicalParams, SolutionData, SolutionType
1414
import jax.numpy as jnp
1515

@@ -277,6 +277,272 @@ def to_dict(self):
277277
return dict_out
278278

279279

280+
@dataclass
281+
class ForwardProblemQuads:
282+
"""
283+
Forward problem for validation of hinge model using a static test on a random quad sample.
284+
The forward perform either a tension, compression, or shear test on the sample in displacement control with clamped top and bottom rows.
285+
"""
286+
287+
# QuadGeometry
288+
n1_blocks: int
289+
n2_blocks: int
290+
spacing: Any
291+
bond_length: Any
292+
horizontal_shifts: Any
293+
vertical_shifts: Any
294+
295+
# Mechanical
296+
k_stretch: Any
297+
k_shear: Any
298+
k_rot: Any
299+
density: Any
300+
damping: Any
301+
302+
# Loading
303+
loading_type: Literal["tension", "compression", "shear"]
304+
amplitude: Any
305+
loading_rate: Any
306+
307+
# Analysis
308+
n_timepoints: int
309+
linearized_strains: bool = False
310+
311+
# Force multiplier (1 or -1 used to compare with experiments according to assumed sign convention)
312+
force_multiplier: float = 1.
313+
314+
# Contact
315+
use_contact: bool = True
316+
k_contact: Any = 1.
317+
min_angle: Any = 0.*jnp.pi/180
318+
cutoff_angle: Any = 5.*jnp.pi/180
319+
320+
# Solution or list of solutions
321+
solution_data: Optional[Union[SolutionType, List[SolutionType]]] = None
322+
323+
# Solver tolerance
324+
atol: float = 1e-8
325+
rtol: float = 1e-8
326+
327+
# Problem name
328+
name: str = "hinge_characterization"
329+
330+
# Flag indicating that solve method is not available. It needs to be set up by calling self.setup().
331+
is_setup: bool = False
332+
333+
def setup(self) -> None:
334+
"""
335+
Set up forward solver.
336+
"""
337+
338+
# Geometry
339+
geometry = QuadGeometry(n1_blocks=self.n1_blocks,
340+
n2_blocks=self.n2_blocks,
341+
spacing=self.spacing,
342+
bond_length=self.bond_length)
343+
block_centroids, centroid_node_vectors, bond_connectivity, reference_bond_vectors = geometry.get_parametrization()
344+
_block_centroids = block_centroids(
345+
self.horizontal_shifts, self.vertical_shifts)
346+
_centroid_node_vectors = centroid_node_vectors(
347+
self.horizontal_shifts, self.vertical_shifts)
348+
_bond_connectivity = bond_connectivity()
349+
_reference_bond_vectors = reference_bond_vectors()
350+
351+
# Damping
352+
damped_blocks = jnp.arange(geometry.n_blocks)
353+
k_ref = self.k_stretch
354+
mass_ref = self.density*geometry.spacing**2
355+
damping_ref = jnp.array([
356+
(k_ref * mass_ref)**0.5,
357+
(k_ref * mass_ref)**0.5,
358+
(k_ref * mass_ref)**0.5 * geometry.spacing**2
359+
]) * jnp.ones((geometry.n_blocks, 3))
360+
damping_values = self.damping * damping_ref
361+
362+
# Applied displacements
363+
constrained_blocks = jnp.concatenate([
364+
jnp.arange(geometry.n_blocks-geometry.n1_blocks,
365+
geometry.n_blocks), # top row
366+
jnp.arange(geometry.n1_blocks) # bottom row
367+
])
368+
constrained_block_DOF_pairs = jnp.array([
369+
jnp.concatenate(
370+
[constrained_blocks, constrained_blocks, constrained_blocks]),
371+
jnp.concatenate([
372+
0*jnp.ones(constrained_blocks.shape, dtype=jnp.int32),
373+
1*jnp.ones(constrained_blocks.shape, dtype=jnp.int32),
374+
2*jnp.ones(constrained_blocks.shape, dtype=jnp.int32)
375+
])
376+
]).T
377+
if self.loading_type == "tension":
378+
loading_vector = jnp.zeros((len(constrained_block_DOF_pairs),))
379+
top_row = jnp.where(constrained_block_DOF_pairs[:, 1] == 1)[
380+
0][:geometry.n1_blocks] # top positions
381+
loading_vector = loading_vector.at[top_row].set(1.)
382+
elif self.loading_type == "compression":
383+
loading_vector = jnp.zeros((len(constrained_block_DOF_pairs),))
384+
top_row = jnp.where(constrained_block_DOF_pairs[:, 1] == 1)[
385+
0][:geometry.n1_blocks] # top positions
386+
loading_vector = loading_vector.at[top_row].set(-1.)
387+
elif self.loading_type == "shear":
388+
loading_vector = jnp.zeros((len(constrained_block_DOF_pairs),))
389+
top_row = jnp.where(constrained_block_DOF_pairs[:, 1] == 0)[
390+
0][:geometry.n1_blocks] # top positions
391+
loading_vector = loading_vector.at[top_row].set(1.)
392+
else:
393+
raise ValueError(
394+
"Loading type should be either tension, compression, or shear!"
395+
)
396+
# Reaction DOFs to compute reaction forces
397+
reaction_block_DOF_pairs = constrained_block_DOF_pairs[top_row]
398+
399+
# Applied displacement
400+
def applied_displacement(t, amplitude, loading_rate):
401+
return amplitude * jnp.where(t < loading_rate**-1, t * loading_rate, 1.)
402+
403+
def constrained_DOFs_fn(t, amplitude, loading_rate):
404+
# Ramp up to target displacement
405+
return loading_vector * applied_displacement(t, amplitude, loading_rate)
406+
407+
# Construct strain energy
408+
strain_energy = build_strain_energy(
409+
bond_connectivity=_bond_connectivity,
410+
bond_energy_fn=ligament_energy_linearized if self.linearized_strains else ligament_energy,
411+
)
412+
contact_energy = build_contact_energy(
413+
bond_connectivity=_bond_connectivity)
414+
potential_energy = combine_block_energies(
415+
strain_energy, contact_energy) if self.use_contact else strain_energy
416+
417+
# Setup solver
418+
solve_dynamics = setup_dynamic_solver(
419+
geometry=geometry,
420+
energy_fn=potential_energy,
421+
constrained_block_DOF_pairs=constrained_block_DOF_pairs,
422+
constrained_DOFs_fn=constrained_DOFs_fn,
423+
damped_blocks=damped_blocks,
424+
atol=self.atol,
425+
rtol=self.rtol,
426+
)
427+
428+
# Analysis params
429+
simulation_time = self.loading_rate**-1
430+
timepoints = jnp.linspace(0, simulation_time, self.n_timepoints)
431+
432+
# Initial conditions
433+
state0 = jnp.zeros((2, geometry.n_blocks, 3))
434+
435+
# Setup forward
436+
def forward(k_values: Tuple[float, float, float]) -> Tuple[SolutionData, ControlParams]:
437+
438+
# Design variables
439+
k_stretch, k_shear, k_rot = k_values
440+
441+
# Define control params
442+
control_params = ControlParams(
443+
geometrical_params=GeometricalParams(
444+
block_centroids=_block_centroids,
445+
centroid_node_vectors=_centroid_node_vectors,
446+
),
447+
mechanical_params=MechanicalParams(
448+
bond_params=LigamentParams(
449+
k_stretch=k_stretch,
450+
k_shear=k_shear,
451+
k_rot=k_rot,
452+
reference_vector=_reference_bond_vectors,
453+
),
454+
density=self.density,
455+
damping=damping_values,
456+
contact_params=ContactParams(
457+
k_contact=self.k_contact,
458+
min_angle=self.min_angle,
459+
cutoff_angle=self.cutoff_angle,
460+
),
461+
),
462+
constraint_params=dict(
463+
amplitude=self.amplitude,
464+
loading_rate=self.loading_rate,
465+
),
466+
)
467+
468+
# Solve dynamics
469+
solution = solve_dynamics(
470+
state0=state0,
471+
timepoints=timepoints,
472+
control_params=control_params,
473+
)
474+
475+
return SolutionData(
476+
block_centroids=_block_centroids,
477+
centroid_node_vectors=_centroid_node_vectors,
478+
bond_connectivity=_bond_connectivity,
479+
timepoints=timepoints,
480+
fields=solution
481+
), control_params
482+
483+
self.solve = jit(forward)
484+
self.geometry = geometry
485+
self.potential_energy = potential_energy
486+
self.elastic_forces = grad(potential_energy)
487+
self.applied_displacement = applied_displacement
488+
self.reaction_block_DOF_pairs = reaction_block_DOF_pairs
489+
self.is_setup = True
490+
491+
def force_displacement(self, solution_data: SolutionData, control_params: ControlParams):
492+
"""
493+
Compute force-displacement data for the given solution data and control params.
494+
"""
495+
if self.is_setup:
496+
displacement_history = solution_data.fields[:, 0]
497+
block_DOF_pairs = self.reaction_block_DOF_pairs
498+
force_history = vmap(
499+
lambda u: jnp.sum(
500+
self.elastic_forces(u, control_params)[
501+
block_DOF_pairs[:, 0],
502+
block_DOF_pairs[:, 1],
503+
]
504+
)
505+
)(displacement_history)
506+
applied_u = self.applied_displacement(
507+
solution_data.timepoints,
508+
**control_params.constraint_params,
509+
)
510+
return jnp.array([applied_u, force_history*self.force_multiplier])
511+
512+
@staticmethod
513+
def from_data(problem_data):
514+
problem_data = ForwardProblem(**problem_data)
515+
problem_data.is_setup = False
516+
return problem_data
517+
518+
def to_data(self):
519+
return ForwardProblem(**dataclasses.asdict(self))
520+
521+
@staticmethod
522+
def from_dict(dict_in):
523+
# Convert solution data to named tuple
524+
if dict_in["solution_data"] is not None:
525+
if type(dict_in["solution_data"]) is dict:
526+
dict_in["solution_data"] = SolutionData(
527+
**dict_in["solution_data"])
528+
elif type(dict_in["solution_data"]) is list:
529+
dict_in["solution_data"] = [SolutionData(
530+
**solution) for solution in dict_in["solution_data"]]
531+
problem_data = ForwardProblem(**dict_in)
532+
problem_data.is_setup = False
533+
return problem_data
534+
535+
def to_dict(self):
536+
# Make sure namedtuples are converted to dictionaries before saving
537+
dict_out = dataclasses.asdict(self)
538+
if type(dict_out["solution_data"]) is SolutionData:
539+
dict_out["solution_data"] = dict_out["solution_data"]._asdict()
540+
elif type(dict_out["solution_data"]) is list:
541+
dict_out["solution_data"] = [solution._asdict()
542+
for solution in dict_out["solution_data"]]
543+
return dict_out
544+
545+
280546
def resample(x, y, n_timepoints):
281547
return jnp.interp(
282548
jnp.linspace(jnp.min(x), jnp.max(x), n_timepoints),

0 commit comments

Comments
 (0)