Using awatif-soil

Guide and API Reference for the Drained Mohr–Coulomb Soil Solver

Mohamed Adil, Awatif

1Scope

awatif-soil is a 2D plane-strain geotechnical finite element solver: six-noded triangles, drained Mohr–Coulomb with a tension cut-off, layered ground with a water table and a K0 initial state, staged excavation with the water that goes with it, and retaining walls with Coulomb soil–wall interfaces and struts, written in C++ and driven from Python. Meshing is not part of it — the caller supplies the mesh, under the conventions of §4. This document is how to use it, ending in a complete reference of every interface (§11). The theory and the benchmark record — including the cross-validation against OpenGeoSys — are in paper.html, which ships alongside it.

The evaluation build is limited to 500 elements — building a bigger model raises ValueError. Everything else is the shipping solver, same API and numerics, and the limit is lifted by the unrestricted build; version() (§11) always tells you which build you have.

2Install

From the unzipped archive directory:

python -m pip install --upgrade awatif-soil==0.4.2 --find-links wheels

pip picks the wheel matching your platform and Python version out of wheels/ itself, and refuses with a clear message if none matches — no filename to choose by hand. Python 3.10 to 3.13; the only runtime dependency is numpy, which comes from PyPI as usual (on a machine with no internet access, install numpy first and add --no-index). For reference, what wheels/ contains:

your platformwheel
Windows, 64-bit...-win_amd64.whl
macOS, Apple Silicon or Intel ...-macosx_11_0_universal2.whl

cp310 to cp313 in the filename are Python 3.10 to 3.13. On Windows, use the standard 64-bit Python from python.org — including on ARM machines, where the x64 build runs under Windows’ built-in emulation; an ARM64 build of Python has no matching wheel here, and pip will say so.

Then run python selftest.py: it runs five numerical checks against closed-form answers and checks the evaluation cap at exactly 500 and 501 elements. It should end with All checks passed.

3First solve

import numpy as np
from awatif_soil import Model, material

clay = material(E=10e3, nu=0.3, c=10.0, phi=25.0, psi=5.0)

model = Model(nodes, elements, clay)      # nodes (n, 2), elements (m, 6)
model.fix(base_nodes, dx=0.0, dy=0.0)     # supports
model.fix(side_nodes, dx=0.0)
model.traction(top_edges, ty=-50.0)       # load

result = model.solve()
if not result.ok:
    raise RuntimeError(result.message)
print(result.displacements)               # (node, 2)
print(result.nodal_stress())              # (node, 4): xx, yy, zz, xy

examples/ has six runnable scripts: a footing driven to collapse (01_footing.py), layered ground under self-weight (02_layered_ground.py), two chained construction phases (03_staged_phases.py), layered ground under a water table started from the K0 procedure and then dewatered (04_ground_conditions.py), a pit dug in two lifts below the water table, flooded and then pumped dry (05_staged_excavation.py), and a braced excavation — walls, a strut, a surcharge, four layers and a dewatered pit, eight phases (06_braced_excavation.py). A guided braced excavation runs in the browser at awatif.co/api/soil/demo, with construction stages, displacements, wall forces and a Python export. It is an illustrative example; a verified same-input Plaxis comparison is still outstanding.

4Conventions

These are the things that most often cost a day if they are assumed rather than read.

5Reading the result

solve() returns a Result whether or not the run converged — displacements, reactions, stresses, the yield map and the per-step history. Every member, with its exact shape, is specified in §11; what needs saying here is how to read a run that stops short.

A stalled solve is not an error and does not raise. Perfect plasticity has a genuine limit load, and past it there is no solution to find. A run that stops short still carries the achieved load_factor, the converged part of the step history, and the displacements that go with it — which is exactly what a load-settlement curve up to failure is made of. Check ok before treating the answer as complete, but do show the partial result: it is the useful part.

To reach a collapse load at all, prescribe displacement rather than force, and pass monitor= to solve() to get the reaction back per step. Force control has no solution past the limit load, by definition. A displacement-controlled collapse run:

clay  = material(E=10e3, nu=0.3, c=10, phi=25, psi=5)
model = Model(nodes, elements, clay)
model.fix(base_nodes, dx=0.0, dy=0.0)
model.fix(side_nodes, dx=0.0)
model.fix(footing_nodes, dy=-0.15)  # displacement, not force
result = model.solve(monitor=footing_nodes)

result.load_factor  # < 1 = incomplete; inspect result.message
result.steps        # load_factor, monitor_force per step: the curve

6Chaining construction phases

A phase declares everything that acts in it and inherits the previous phase’s stress through initial_stress:

phase2 = Model(nodes, elements, clay)
phase2.fix(base_nodes, dx=0.0, dy=0.0)        # everything that acts in this phase:
phase2.traction(top_edges, ty=-50.0)          #   the load phase 1 applied, still there
phase2.traction(new_edges, tx=-10.0)          #   and the one this phase adds
phase2.phreatic(-2.0)                         #   the water table (unchanged, or not)
phase2.initial_stress(phase1_result.state)    # where phase 1 left the soil

The solver reads what the inherited stress already carries straight off the stress and ramps only the difference between that and what the phase declares. So a load present in both phases is held, not re-applied; a load that this phase leaves out is unloaded, proportionally over the phase; a load that this phase adds is applied; and a water table that moved, or self-weight switched on, is ramped like any other load. This is how Plaxis defines a phase, and it is what lets self-weight and water be carried across phases at all.

The consequence to remember: a load left out is removed. Omit phase 1’s surcharge from phase 2 and it is quietly unloaded — the solve still converges and still reports Ok, and plasticity is path dependent, so the answer is simply wrong. examples/03_staged_phases.py shows the difference side by side. Declare, in every phase, everything that acts in it.

Displacements are reported per phase; Result.total_displacements adds them up across the phases chained through the state. Result.state also carries the load that was acting at the end of the phase, which is what keeps reactions exact while the next phase is under way; a plain list of Gauss-point states is accepted too, in which case reactions are exact at the end of a phase and approximate before, and the total restarts.

7Layers, water and initial stress

Layers are materials with material_ids. Water is a phreatic line per phase. The initial state comes either from gravity loading — a phase with gamma set and no inherited state, which switches self-weight on from zero stress and gives the elastic lateral ratio ν/(1−ν) — or from the K0 procedure, which prescribes the lateral stress instead:

sand = material(E=30e3, nu=0.25, c=0.0, phi=34.0, psi=4.0,
                gamma=17.0, gamma_sat=20.0, k0=0.45)
clay = material(E=8e3, nu=0.35, c=15.0, phi=22.0, psi=0.0,
                gamma=16.0, gamma_sat=18.0)           # k0 defaults to 1 - sin phi

model = Model(nodes, elements, [sand, clay], material_ids)
model.fix(base_nodes, dx=0.0, dy=0.0)
model.fix(side_nodes, dx=0.0)
model.phreatic(-1.0)                          # water table 1 m down; gamma_w = 10

model.initial_stress(model.k0_state())        # K0 procedure: the initial state
result = model.solve()                        # nil-step: nothing moves

phase2 = Model(nodes, elements, [sand, clay], material_ids)
phase2.fix(base_nodes, dx=0.0, dy=0.0)
phase2.fix(side_nodes, dx=0.0)
phase2.phreatic(-4.0)                         # lower the water: ramped as a load
phase2.initial_stress(result.state)
dewatered = phase2.solve()

8Staged excavation

Excavation is element activation on top of the phase rule of §6. A phase declares which elements exist; an element left out has no stiffness, no weight, no water and no stress, and a phase that inherits stress from one in which it was active removes, ramped over the phase, the force that soil exerted on what remains. Nothing else is needed to dig:

lift1 = Model(nodes, elements, clay)
lift1.fix(base_nodes, dx=0.0, dy=0.0)
lift1.fix(side_nodes, dx=0.0)
lift1.phreatic(-1.0)                          # the retained water table
lift1.deactivate(pit_elements_above(-2.0))    # dig the first lift: those elements are gone
lift1.initial_stress(initial.state)           # the ramp removes the force they exerted
result1 = lift1.solve()

lift2 = Model(nodes, elements, clay)
lift2.fix(base_nodes, dx=0.0, dy=0.0)
lift2.fix(side_nodes, dx=0.0)
lift2.phreatic(-1.0)
lift2.deactivate(pit_elements_above(-4.0))    # both lifts: soil that is dug stays dug
lift2.phreatic(-4.0, elements=pit_column)     # pit and ground under it: pumped to the floor
lift2.initial_stress(result1.state)
result2 = lift2.solve()

result2.active                # (element,) bool
result2.total_displacements   # accumulated over every phase
lift2.surface_water()         # the water standing on the boundary, (node, 2)

9Walls, interfaces and struts

A wall is declared on a line of mesh nodes the mesh conforms to — every consecutive (corner, mid, corner) triple an edge with soil on both sides — and the solver does the rest: the soil on each face gets its own copy of the nodes along the line, the wall keeps the originals, and a zero-thickness interface joins each pair of faces. Struts are bars between two nodes. Both are declared in every phase, active or not, exactly like elements, so the node numbering is the same throughout:

chain = nodes_on_line(x=3.5, top=18.5, toe=9.5)   # mesh nodes, top first, mid-sides included

initial = Model(nodes, elements, layers, material_ids)
initial.fix(base_nodes, dx=0.0, dy=0.0); initial.fix(side_nodes, dx=0.0)
initial.phreatic(17.26)
initial.wall(chain, EA=1.8e6, EI=2.3e4, R_inter=0.67, active=False)   # declared, not yet built
initial.strut(wall_node, axis_node, EA=2.7e6, spacing=3.0, active=False)
state = initial.k0_state()                    # ground alone: structures must be inactive

install = Model(nodes, elements, layers, material_ids)
install.fix(base_nodes, dx=0.0, dy=0.0); install.fix(side_nodes, dx=0.0)
install.phreatic(17.26)
install.wall(chain, EA=1.8e6, EI=2.3e4, R_inter=0.67)   # active: the tie is released, the
install.strut(wall_node, axis_node, EA=2.7e6, spacing=3.0, active=False)  #   earth pressure ramps onto it
install.initial_stress(state)
result = install.solve()

wall = result.wall(0)                         # .N .Q .M per metre at the nodes, .displacements, .rotations
face = result.interface(0, "right")           # declared top-down, right is -x: .normal .shear .mode
install.wall_water()                          # the water on the wall, (node, 2)

dig = Model(nodes, elements, layers, material_ids)
# ... supports, water, wall(active), then:
dig.strut(wall_node, axis_node, EA=2.7e6, spacing=3.0)   # installed: carries what moves from here
dig.deactivate(pit_elements_above(13.5))
dig.phreatic(13.5, elements=pit_column)       # pumped dry; the wall takes the unbalanced water
dig.initial_stress(result.state)
result = dig.solve()
result.strut(0)                               # kN per strut, tension positive

10Known limitations in this stage

Stated plainly, because they matter for how far you can take the integration:

11API reference

Everything importable is listed here. The public surface is deliberately small: material, Model, Result, State, K0State, WallResult, InterfaceResult, Status, Region, InterfaceMode and version, plus a few module constants. State(states, applied=None, displacements=None, walls=None, interfaces=None, struts=None) can also be built by hand. Anything else reachable under awatif_soil._soil is internal and carries no compatibility promise. Arrays go in and come out as numpy; shapes are written (rows, columns).

material()

material(E, nu, c, phi, psi, sigma_t=0.0, gamma=0.0,
         gamma_sat=None, k0=None) -> MaterialMC

A drained Mohr–Coulomb material. All stiffness and strength parameters are effective.

parametermeaning accepted
EYoung’s modulus> 0
nuPoisson’s ratio0 ≤ ν < 0.5
ccohesion c′≥ 0
phifriction angle φ′, degrees 0 ≤ φ′ < 90
psidilation angle ψ, degrees 0 ≤ ψ ≤ φ′
sigma_ttension cut-off default 0 (no tension); math.inf disables it
gammaunit weight above the phreatic line, force per unit volume, acting in −y≥ 0; default 0 (weightless)
gamma_satunit weight below the phreatic line ≥ 0; default: same as gamma
k0lateral earth pressure coefficient for k0_state()> 0; default 1 − sin φ′

Any violated bound raises ValueError. psi = phi is associated flow — accepted, but for soil it grossly overpredicts both dilation and collapse load. Without a cut-off (sigma_t=math.inf) the bare Mohr–Coulomb pyramid carries isotropic tension all the way to its apex at c′ cot φ′. Self-weight given through gamma is a load like any other: with no inherited state it is ramped from zero, so a solve with gamma set is gravity being switched on; with an inherited state that already carries it, it is held (§6).

On the returned MaterialMC object, phi and psi are stored in radians; build materials with material() rather than mutating the object’s attributes.

Model()

Model(nodes, elements, materials, material_ids=None)

nodes is (n, 2) float coordinates. elements is (m, 6) integer connectivity in the gmsh / Abaqus CPE6 node order of §4. materials is a single MaterialMC or a sequence of them; with more than one, material_ids is required — one index into materials per element, in element order (examples/02_layered_ground.py shows a two-material column).

A wrong shape, a missing material_ids, or a model over the evaluation element cap raises ValueError — the cap fires here, at construction, not after a solve you have already waited for. Model.node_count is the number of nodes, Model.element_count the number of elements. The module constants NODES_PER_ELEMENT = 6 and POINTS_PER_ELEMENT = 3 (Gauss points per element) name the two layout numbers the shapes below are built from; GAMMA_WATER = 10.0 is the default unit weight of water.

Supports and loads

model.fix(nodes, dx=None, dy=None, sides=True)
model.fix_rotation(nodes)
model.point_load(node, fx=0.0, fy=0.0)
model.traction(edges, tx=0.0, ty=0.0)

Each returns the model, so calls chain. Loads accumulate across calls. Together with self-weight and water they make up what the phase declares; the solver ramps whatever differs from the inherited state (§6) — from zero when there is none. Prescribed displacements are ramped from zero in every phase. A point load or a traction on a node no active element touches makes solve() return Status.InvalidMesh (§8); a prescribed displacement there is ignored.

sides matters only on a node a wall runs through: True (the default) holds the wall and both soil faces — a boundary — and False holds the wall alone, which is how a wall is given a prescribed displacement. fix_rotation holds the wall rotation at the given nodes at zero, Plaxis’s rotation fixity; ignored where no active wall reaches the node. A traction on an edge that ends at a wall loads the side the edge belongs to; one on the wall line itself raises ValueError.

Ground water

model.phreatic(level, gamma_w=10.0, elements=None)
model.pore_pressure(values)
model.pore_water() -> (ndarray (element, 3), ndarray (element, 3))
model.surface_water() -> ndarray (node, 2)
model.wall_water() -> ndarray (node, 2)

wall_water() is the water of the soil on each side of a wall pressing on the wall, as nodal forces on the wall nodes — minus each face’s share of surface_water(). Applied automatically; this shows it.

Staged excavation

model.deactivate(elements)
model.activate(elements)
model.active -> ndarray (element,) bool

deactivate leaves the given elements (indices into the connectivity) out of this phase and activate puts them back; every element starts active, and an index out of range raises ValueError. What that means for stress, nodes, loads and water is §8. Both return the model.

Walls, interfaces and struts

model.wall(nodes, EA, EI, w=0.0, nu=0.0, R_inter=1.0, kn=None, ks=None,
           active=True, interface=True, toe="free") -> int
model.strut(a, b, EA, spacing=1.0, prestress=None, active=True) -> int
model.nodes -> ndarray (node, 2)      model.node_origin -> ndarray (node,)
model.node_count, model.mesh_node_count, model.walls, model.struts -> int
model.elements -> ndarray (element, 6)

wall declares a wall along nodes, the chain of mesh nodes from one end to the other, mid-side nodes included (an odd count of at least three), and returns its index. Each consecutive (corner, mid, corner) triple must be an edge with soil on both sides, walls may not share nodes, and the parameters are checked (EA > 0, EI ≥ 0, w ≥ 0, 0 ≤ nu < 0.5, 0 < R_inter ≤ 1); anything else raises ValueError. What it builds and what the arguments mean is §9. The split happens as the wall is declared: node_count, nodes, node_origin and elements reflect it at once, and mesh_node_count stays the mesh’s. strut declares a bar between two mesh nodes with the axial stiffness of one strut and its out-of-plane spacing, returning its index; prestress is the axial force to impose in this phase, tension positive.

Result.wall(), Result.interface(), Result.strut()

result.wall(index=0) -> WallResult
result.interface(wall=0, side="left") -> InterfaceResult
result.strut(index=0) -> float        result.strut_forces -> ndarray (strut,)
result.rotations -> ndarray (node,)
membertype / shape meaning
WallResult.nodes(k,) the wall’s mesh nodes in declared order, ends and middles
.coordinates, .s(k, 2), (k,) where they are, and the arc length from the first node
.displacements, .rotations(k, 2), (k,) this phase’s movement of the wall
.N, .Q, .M(k,) section forces per metre of wall at the nodes, extrapolated from the two Gauss points of each element and averaged where elements meet; N tension positive, Q and M in the element frame (§9)
.gauss(element, 2, 3) the raw (N, Q, M) at the Gauss points
InterfaceResult.side, .nodes, .wall_nodes str, (k,), (k,) which face; the soil-side node of each pair (a copy, or the shared toe) and the wall node
.normal, .shear(k,) effective normal stress (compression negative) and shear stress at the pairs, averaged where two elements share a pair
.gap, .slip(k,) accumulated opening and plastic shear displacement
.modelist of InterfaceMode per pair, the more plastic of the two elements’ where they share it; .raw (element, 3, 4) and .raw_mode are the per-element values
strut(i), strut_forcesfloat, (strut,) axial force per strut (the per-metre force times the spacing), tension positive; in a prestress phase, the force imposed so far
rotations(node,) wall rotation, counter-clockwise positive; zero off the walls

Construction phases and initial stress

model.k0_state() -> K0State
model.initial_stress(state)
model.applied_force() -> ndarray (node, 2)

k0_state runs the K0 procedure on this model’s ground profile and water (§7), over the active elements, and returns a K0State: .states (opaque), .applied (the self-weight and water it balances), .violations (Gauss points outside the yield surface; a warning is issued when it is not zero) and .stress(), (element, 3, 4). It raises ValueError for a non-horizontal exposed surface, non-horizontal layers, or a cluster phreatic line; water above the surface is standing water and is accepted. Point loads and tractions on the model play no part.

initial_stress takes the previous phase’s Result.state or a K0State — or a plain sequence of Gauss-point states, in which case the reactions of this phase are exact at its end and approximate before, and the displacement total restarts. Anything but 3 × n_elements states raises ValueError, and so does a state whose walls or struts do not match the ones declared — declare the same walls and struts, in the same order, in every phase. applied_force() returns this model’s own external load at load factor 1, (node, 2) — point loads, tractions, self-weight, water, wall weight and strut prestress assembled to nodes — and is informational: the solver works it out for itself. initial_force, from release 0.1.0, is gone; calling it raises TypeError naming the replacement.

Model.solve()

model.solve(monitor=None, **settings) -> Result

monitor is a set of node indices whose vertical reaction is summed and reported per step as StepRecord.monitor_force. This is how a load-displacement curve is obtained under displacement control: the total reaction over the whole boundary is zero by equilibrium, so a curve needs the reaction on a named subset — the loaded footing, say.

**settings overrides the solver settings below; an unknown name raises TypeError. The defaults are what every benchmark in the paper ran on, and exist to be read, not tuned.

settingdefault meaning
tolerance1e-6 relative residual for step convergence
max_iterations60 Newton iterations per load step. Generous on purpose: strongly non-associated flow needs 10–15 near the limit load, and a step that gives up early triggers a bisection cascade costing far more than the iterations saved.
max_steps200 total load steps, bisected attempts included
initial_load_fraction0.25 the first load increment, as a fraction of the total; also the cap increments grow back to after bisection
min_load_fraction1e-4 the smallest increment bisection may take; failure there ends the run as NotConverged with the achieved load factor
apex_tangent_floor1e-6 floor on the tangent stiffness at the Mohr–Coulomb apex, where the exact elastoplastic tangent is zero
gap_tangent_fraction1e-3 the tangent of an open interface pair, as a fraction of its closed stiffness
fallback_iterations, fallback_stall_window 3000, 100 the elastic-soil fallback a load step gets after the consistent Newton iteration has failed it (§10), in a model with walls, interfaces or struts — a soil-only model bisects as before: it iterates while it is getting somewhere, stopping when the residual has not fallen by a tenth over the window; 0 iterations bisects straight away
fallback_tolerance1e-4 the relative residual accepted where the fallback has stalled or spent its budget — a hundred times tighter than Plaxis’s 1% — rather than bisecting; never used while it is still converging, and a step accepted this way is marked fallback in its record with its residual
divergence_factor0 optional early exit: a consistent iteration whose residual has grown past this multiple of its first stops and hands over to the fallback; 0 never does, because a plastic step legitimately climbs by orders of magnitude on its first correction
line_search_cuts, line_search_growth0, 10 a diagnostic blow-up guard, off by default: with cuts > 0, a Newton step that raises the residual by more than the growth factor is halved up to that many times
traceFalse print every Newton iteration to stderr
symmetriseFalse replace the non-symmetric tangent by its symmetric part. A fallback, not a speed-up: non-associated flow genuinely breaks symmetry, and on the strongly non-associated benchmark the symmetrised tangent stalls at a fraction of the target settlement (measured in the paper).
monitor_dofs[] what monitor= sets: DOF indices (2·node + 1 for vertical) whose reaction is summed into monitor_force

Result

Returned by solve() whether or not the run converged (§5). Attributes first, then the methods that derive fields from the Gauss-point state.

membertype / shape meaning
okbool status == Status.Ok; check before treating the answer as complete
statusStatus see the enum below
messagestr detail when status is not Ok — what stopped, and at which load factor
load_factorfloat fraction of the load actually applied; less than 1 means it stalled
displacements(node, 2) ux, uy for this phase, over Model.nodes (the mesh’s nodes, then the copies a wall made); zero at nodes no active element touches
total_displacements(node, 2) accumulated over every phase chained through the state, counted from when each element’s soil was placed (§8)
reactions(node, 2) support reactions, zero at unconstrained DOFs — the total reaction, history included, not the phase’s share; effective at a submerged support, the water on it being an applied load
active(element,) bool which elements took part in this phase
applied(node, 2) the external load in effect at the end of the phase
stepslist of StepRecord every load step, bisected attempts included — filter on converged
stateState end state per Gauss point, per wall, interface and strut, plus the load acting and the displacement total; pass to the next phase’s initial_stress. State.stress() reads the soil as (element, 3, 4); State.displacements is the total; .walls, .interfaces, .struts are opaque
gauss_stress()(element, 3, 4) σ′xx, σ′yy, σ′zz, τxy per Gauss point, effective — where stress actually lives; zero in an inactive element
nodal_stress(material=None)(node, 4) the same components, exactly extrapolated (the Gauss field over a T6 is linear) and area-averaged across the active elements — over one material’s elements only when material is given, which is the right thing to contour at a layer boundary; a node no contributing element touches is NaN
plastic_points()(element, 3) bool which Gauss points are yielding
regions()(element, 3) Region which part of the yield surface each point returned to

StepRecord

One entry per attempted load step, all read-only. converged is False on a bisected attempt; failed attempts are kept on purpose — they are how the load path near a limit load is diagnosed — so a curve must filter on it rather than take every record. Note the name collision: StepRecord.plastic_points is a count, Result.plastic_points() is the boolean map.

fieldmeaning
convergeddid this step converge
load_factorattempted target load factor; achieved only when converged
residual_normabsolute Euclidean norm of the free-DOF residual at exit
relative_residualresidual norm divided by the equilibrium reference scale
relaxedconverged using the fallback tolerance, above the requested strict tolerance. Set fallback_tolerance=tolerance to require strict convergence.
iterationsNewton iterations used
plastic_pointsnumber of yielding Gauss points
apex_pointsof those, how many returned to the apex
slip_points, gap_pointsinterface pairs sliding, and open
fallbackthe step ran under the elastic soil matrix (§10)
line_searchesiterations the blow-up guard shortened; zero unless it is switched on
monitor_forcesummed reaction over the monitored DOFs (§11, solve)
assemble_seconds, factorize_seconds, solve_secondswhere the step’s time went: assembly, numeric factorisation, triangular solves

Status, Region and InterfaceMode

Statusmeaning
Okfully converged at load factor 1
InvalidMeshthe mesh or its data was rejected before solving; message says exactly what
NotConvergedstopped short of the full load — a limit load reached under bisection, or the step budget exhausted; message carries the achieved load factor, and the partial result is still returned (§5)
SingularMatrix, SolveFailedreserved for factorisation and linear-solve failures; in the current solver such a failure is retried under bisection and surfaces as NotConverged

Region names where the return map left each Gauss point’s stress, with principal stresses ordered σ1 ≥ σ2 ≥ σ3, tension positive:

Regionmeaning
Elasticinside the yield surface; no return
McFaceon one Mohr–Coulomb plane
McEdgeCompressionon the hexagon edge σ1 = σ2 (triaxial compression)
McEdgeExtensionon the edge σ2 = σ3 (triaxial extension)
McApexat the apex; all six planes active and the elastoplastic tangent exactly zero (hence apex_tangent_floor)
CutOffFaceon the tension cut-off plane σ1 = σt
McCutOffCorneron the intersection of a Mohr–Coulomb plane and the cut-off
McCutOffVertexan intersection of a Mohr–Coulomb edge and the tension cut-off; principal stresses fixed, with rotation included in the spatial tangent
CutOffEdgeσ1 = σ2 = σt
CutOffVertexσ1 = σ2 = σ3 = σt
Failedno return satisfying the flow rule. For example, zero dilation without a tension cut-off cannot remove hydrostatic tension. Local stress integration raises RuntimeError. The global solver retries shorter steps and returns NotConverged with committed history if no admissible continuation is found.

InterfaceMode is the regime of an interface pair in a phase:

InterfaceModemeaning
Offthe soil on this side is excavated; the pair is not connected
Tiedthe interface, or its wall, is inactive: the pair moves as one node (continuous soil, or a bonded wall)
Elasticclosed, inside Coulomb’s line
Slipclosed and sliding at Rinter (c′ − σ′n tan φ′)
Gapopen under the tension cut-off; no traction

version()

>>> import awatif_soil
>>> awatif_soil.version()
'awatif-soil a1b2c3d | eigen fetched 3.4.0 @ 3147391d | Release | evaluation build, 500 element limit'

Build provenance: the git commit the binary was built from, the exact Eigen it was linked against, the build type, and — on an evaluation build — the element limit (§13 asks for this line in every bug report).

12Licence and notices

LICENSE.txt governs your use of this software. THIRD_PARTY.md carries the notices for the two open-source libraries this binary is built with, Eigen and pybind11, both used unmodified.

13Support

Questions and bug reports: [email protected]. Please include the full version() line, your OS and Python version, and the output of selftest.py.