2. Solver API
This section will describe Gustave's low-level API: Solvers.
In this tutorial we'll aim at creating the following structure, and computing its force distribution:
Note
In this tutorial, all quantities are interpreted in the metric system. This is not mandatory when using the unitless
distribution. You can interpret them in another natural system of unit: if you choose to interpret 1 unit of length as an imperial foot
, 1 unit of mass as an imperial pound
, and 1 unit of time as a second
, then 1 unit of force must be interpreted as a pound.foot/second²
.
Choosing a distribution
First, let's choose the std::unitless
distribution with double
precision, as explained in getting started:
// Choosing the Std Unitless distribution, with double precision
#include <gustave/distribs/std/unitless/Gustave.hpp>
using G = gustave::distribs::std::unitless::Gustave<double>;
We'll also define some convenient type aliases:
using Structure = G::Solvers::Structure;
using Solver = G::Solvers::F1Solver;
Create a new solver structure
To create an empty Structure
, just use its default constructor. We'll store ours in a std::shared_ptr, as it will be required later by the solver.
auto structure = std::make_shared<Structure>();
std::cout << "Structure of " << structure->nodes().size() << " blocks\n";
std::cout << "Structure of " << structure->links().size() << " links\n";
Expected output:
Structure of 0 blocks
Structure of 0 links
Add blocks to a structure
Use Structure::addNode(...)
. This takes a Structure::Node
object, whose constructor has 2 arguments:
- mass: the mass of the block (in kilogram)
- isFoundation: a boolean to indicate if this block is a foundation
It returns a NodeIndex
, a unique identifier of this block in the structure. These indices are generated sequentially (0,1,2,...).
auto const blockMass = 3'000.0; // kilogram
// xy
auto const n00 = structure->addNode(Structure::Node{ blockMass, true });
auto const n01 = structure->addNode(Structure::Node{ blockMass, false });
auto const n02 = structure->addNode(Structure::Node{ blockMass, false });
auto const n12 = structure->addNode(Structure::Node{ blockMass, false });
auto const n22 = structure->addNode(Structure::Node{ blockMass, false });
auto const n21 = structure->addNode(Structure::Node{ blockMass, false });
auto const n20 = structure->addNode(Structure::Node{ blockMass, true });
std::cout << "Structure of " << structure->nodes().size() << " blocks\n";
std::cout << "Structure of " << structure->links().size() << " links\n";
Expected output:
Structure of 7 blocks
Structure of 0 links
Add links to a structure
Use Structure::addLink(...)
. This takes a Structure::Link
object, whose constructor has 4 arguments:
- localNodeId: the index of the first node
- otherNodeId: the index of the second node
- normal: the normal vector on the surface of localNode
- conductivity: a compression/shear/tensile quantity in
Newton/metre
. This is used by the solver to compute a force distribution: the higher the 3 components are, the more force will be transmitted through this link. It should be proportional to the maximum nominal force/pressure of the contact surface, and inversely proportional to the distance between the 2 nodes.
It sequentially returns a LinkIndex
, which uniquely identifies this link in the structure.
// { compression, shear, tensile } in Newton/metre
auto const wallConductivity = G::Model::ConductivityStress{ 1'000'000.0, 500'000.0, 200'000.0 };
auto const roofConductivity = G::Model::ConductivityStress{ 100'000.0, 500'000.0, 100'000.0 };
auto const plusY = G::NormalizedVector3{ 0.0, 1.0, 0.0 };
auto const plusX = G::NormalizedVector3{ 1.0, 0.0, 0.0 };
// left wall
auto const l00_01 = structure->addLink(Structure::Link{ n00, n01, plusY, wallConductivity });
structure->addLink(Structure::Link{ n01, n02, plusY, wallConductivity });
// right wall
auto const l20_21 = structure->addLink(Structure::Link{ n20, n21, plusY, wallConductivity });
structure->addLink(Structure::Link{ n21, n22, plusY, wallConductivity });
// roof
structure->addLink(Structure::Link{ n02, n12, plusX, roofConductivity });
structure->addLink(Structure::Link{ n12, n22, plusX, roofConductivity });
std::cout << "Structure of " << structure->nodes().size() << " blocks\n";
std::cout << "Structure of " << structure->links().size() << " links\n";
Here we stored 2 link indices for later use: l00_01
(between foundation n00
and block n01
) and l20_21
(between foundation n20
and block n21
).
Expected output:
Structure of 7 blocks
Structure of 6 links
Note: It is possible to mix calls to Structure::addLink()
and Structure::addNode()
. The only requirement is that when addLink()
is called, the nodes in the new link are already declared.
Configure a solver
For now the only solver available is the F1Solver
, which generate force vectors that are always colinear with the gravity acceleration vector g
.
The two mandatory parameters for the solver are:
- g: the gravity acceleration vector in
metre/second²
- solverPrecision: the precision of the solver. The closer to 0 it is, the more accurate the solutions will be according to Newton's 1st law of motion (the net force of non-foundation blocks will be closer to 0).
[[nodiscard]]
static Solver newSolver() {
auto const g = G::vector3(0.f, -10.f, 0.f); // gravity acceleration (metre/second²).
auto const solverPrecision = 0.01; // precision of the force balancer (here 1%).
return Solver{ Solver::Config{ g, solverPrecision } };
}
auto const solver = newSolver();
std::cout << "Solver gravity vector = " << solver.config().g() << '\n';
std::cout << "Solver target max error = " << solver.config().targetMaxError() << '\n';
Expected output:
Solver gravity vector = { "x": 0, "y": -10, "z": 0 }
Solver target max error = 0.01
Solve a structure
Simply use F1Solver::run(structure)
. This returns a SolverResult
object with a isSolved()
method, indicating if a solution within the target solverPrecision
was reached.
auto const solverResult = solver.run(structure);
std::cout << "solution.isSolved() = " << solverResult.isSolved() << '\n';
Expected output:
solution.isSolved() = 1
Warning
Do not modify structure
after passing it to the solver.
Inspect a solution's force vectors
If a SolverResult
object is solved, the solution()
method returns a Solution
object. This object provides convenient methods to inspect nodes, contacts, and forces.
auto const& solution = solverResult.solution();
std::cout << "Force vector on block 00 by 01 = " << solution.contacts().at({l00_01, true}).forceVector() << '\n';
std::cout << "Force vector on block 21 by 22 = " << solution.contacts().at({l20_21, false}).forceVector() << '\n';
A few comments on this block of code:
solution.contacts()
returns a range to access any link/contact through aContactReference
.at({l00_01, true})
gets us the contact from itsContactIndex
. This contact index is made from the link indexl00_01
, and atrue
boolean indicating that we want the contact on the surface oflocalNodeId
, son00
in this case.forceVector()
gets the force vector acting on the surface ofn00
fromn01
, transmitted through linkl00_01
Possible output:
Force vector on block 00 by 01 = { "x": 0, "y": -75000, "z": 0 }
Force vector on block 21 by 22 = { "x": -0, "y": 75000, "z": -0 }
Comments about this output:
- line 1: This is the force of contact index
{l00_01, true}
, so the force on the foundationn00
fromn01
. Since the structure is symmetrical, we expect this force to be half the total weight of the structure. Total weight of the structure:5 * blockMass * g = {0, -150'000, 0} Newton
- line 2: This is the force of contact index
{l20_21, false}
. Thefalse
inverses thelocalNodeId
andotherNodeId
of the contact: so this is the force onn21
by the foundationn20
. The foundation preventsn21
from falling by pushing in the opposite direction of the gravity vectorg
. So the Y coordinate is positive.