rlx-fdm 0.2.0

Force density method (FDM) for RLX — pin-jointed form-finding ported from jax_fdm.
Documentation

rlx-fdm

Force density method (FDM) for pin-jointed structures in RLX, with logic ported from jax_fdm.

Borrowed from jax_fdm

jax_fdm module rlx-fdm
datastructures / FDNetwork network::Network
equilibrium/structures (connectivity, free/fixed split) structure::Structure
equilibrium/models (stiffness_matrix, load_matrix, nodes_equilibrium) equilibrium
equilibrium/fdm (fdm, validation) reference::fdm
equilibrium/states state::EquilibriumState
goals/edge/length, goals/network/loadpath goals, objective::Goal
constraints + constrained_fdm constraints, optimize::constrained_fdm
data/json/*.json io::json (feature io)
equilibrium/sparse + custom VJP Planned: rlx-sparse + symmetric solve (K is SPD for physical q)
constrained_fdm + optimizers constrained_fdm (GD or L-BFGS on q, optional L2 on q)
Visualization (COMPAS / compas_view2) Out of scope

Quick start

use rlx_fdm::{Network, fdm};

let network = Network::arch_chain(5.0, 10, -1.0, -0.2);
let eq = fdm(&network).expect("equilibrium");
// Free-node z should sag negative under vertical load.
assert!(eq.xyz[[1, 2]] < 0.0);

Features

Feature Description
reference (default) Dense CPU FDM solve
io Load jax_fdm-style JSON networks
ir Emit Graph with Op::DenseSolve for RLX autodiff

Sparse + nonlinear

use rlx_fdm::{Network, fdm_with_options, FdmOptions};

let mut net = Network::arch_chain(5.0, 10, -1.0, 0.0);
net.edges_load_uniform([0.0, 0.0, -0.05]);

let opts = FdmOptions::nonlinear(50, 1e-6, true);
let eq = fdm_with_options(&net, &opts)?;
  • sparse: SparseStiffness CSR + PCG (sparse.rs, solve.rs)
  • nonlinear: fixed-point x ← K⁻¹ P(x) with edge loads × length (iterative.rs, loads.rs)

Deferred features (implemented)

jax_fdm rlx-fdm
EquilibriumStructureSparse / index_array SparseStiffnessFast — O(nnz) assembly
Face tributary loads loads + mesh::MeshStructure
solver_fixedpoint_implicit grad_loss_wrt_q_fixedpoint — unrolled adjoint (edge + face loads, local LCS)
LBFGSB lbfgs::Lbfgs + OptimizerKind::Lbfgs
Mesh goals MeshArea, MeshPlanarity, MeshFaceRectangular, MeshLaplacian
PCG adjoint solve_adjoint_columns + grad_loss_wrt_q_linear_with_solver
Support design params grad_loss_wrt_xyz_fixed_linear (dL/dX_fixed)
rlx_sparse PCG rlx_op (feature rlx-sparse)

Inverse design

cargo run -p rlx-fdm --example inverse_design
cargo run -p rlx-fdm --example constrained_arch
  • inverse_design: manual GD on q for one edge-length target.
  • constrained_arch: constrained_fdm with NetworkLoadpath + EdgeLength goals and EdgeQ / length caps.
use rlx_fdm::{constrained_fdm, Constraint, Goal, Network, OptimizeConfig};

let net = Network::arch_chain(5.0, 10, -1.0, -0.2);
let goals = vec![Goal::network_loadpath(12.0, 1.0)];
let constraints = vec![Constraint::all_edge_q(-50.0, -0.5, 0.01)];
let cfg = OptimizeConfig::arch_form_finding(); // L-BFGS + linear FDM + sparse PCG
let res = constrained_fdm(&net, &goals, &constraints, &cfg)?;

End-to-end loop (one iteration): equilibrium solve → goal + constraint grads → implicit dL/dq using EquilibriumModel::pack_xyz_free (no extra fixed-point re-solve). Topology is cached in OptWorkspace; L-BFGS line search trials only vary q.

Fused MIR (feature ir, fuse_mir): FusedEquilibriumRunner compiles q → x_f once per topology and reuses it inside constrained_fdm (enabled by default in OptimizeConfig::arch_form_finding). With feature fuse, FusedAutodiffFormFinding adds loss + dL/dq through sparse PCG in one compiled graph.

Chain rule: goals_grad_xyz_free + constraint grads → grad_loss_wrt_q (implicit adjoint).

Design parameters, losses, optimizers (jax_fdm 1–5)

Goals (jax_fdm subset)

Goal Description
NetworkLoadpath Total load path energy
EdgeLength / MeanEdgeLength Edge length targets
EdgeForce / MeanEdgeForce Axial force targets
NodeCoord / MinFreeZ Node position / sag
Residual Equilibrium misfit penalty
MeshArea / MeshPlanarity Total face area / mean planarity (needs mesh on Network)
MeshFaceRectangular / MeshLaplacian Quad corner orthogonality / edge spring smoothing

Mesh I/O (feature io)

use rlx_fdm::io::{from_json_path, mesh_from_json_str, merge_mesh, to_json_str, MeshDocument};

let mut net = from_json_path("data/quad_mesh.json")?;
let mesh = mesh_from_json_str(r#"{"faces":[[0,1,2]],"faces_load":[[0,0,-1]]}"#)?;
merge_mesh(&mut net, &mesh);
let json = to_json_str(&net)?;

MIR optimizer hook (feature ir + rlx-sparse)

With features = ["ir", "rlx-sparse"], FdmMirOptimizer::build_equilibrium_graph wires fdm_q → fdm.assemble_csr_values → rlx_sparse.pcg_solve (×3) with param fdm_P. Autodiff through the graph matches the host PCG adjoint in solve_adjoint_columns.

MIR optimizer hook (feature ir)

use rlx_fdm::mir_opt::{FdmMirOptimizer, FdmGradMode, goals_grad_wrt_q};
use rlx_fdm::graph::{fdm_dense_graph, build_grad_q_custom_fn, FdmGradQSignature};

let opt = FdmMirOptimizer { grad_mode: FdmGradMode::FixedPoint(cfg), ..Default::default() };
let gq = goals_grad_wrt_q(&opt, &network, &goals, None)?;

build_grad_q_custom_fn emits fdm_dq param; fill with set_grad_q_param before Session::run for host dL/dq in fused graphs.

RLX integration path

  1. Today: nonlinear fixed-point adjoint (edge loads), full goal library, mesh JSON, FdmMirOptimizer + constrained_fdm.
  2. Next: IPOPT interface (external); tighter PCG-VJP vs host linear adjoint.

Anderson acceleration: set IterativeConfig::anderson_depth > 0 on nonlinear solves. Implicit adjoint unrolls a full tmax trajectory (eta = 0) so it matches central-difference dL/dq.