Python API
A geotechnical finite element analysis is a Python script.
katai is the Python package of KATAI 2D. A script reads like the
engineering problem — materials, geometry, water, loads, structures, phases — and every
object is created in one call that hands back a handle. Phases switch handles on and off.
That is the whole of staged construction, and it makes a parametric study a
for loop.
A footing, its settlement and its factor of safety
No input file, no project folder, no session to keep alive. The script on the right builds a strip footing on clay, loads it in a second phase, and asks for the factor of safety of the loaded state. The three comment lines at the bottom are what the published 0.9.0 wheel prints, in about forty seconds: no displacement in the K0 state, 24.7 mm under the footing, a factor of safety of 1.78. (A fos of −1 means the phase did not compute one; the displacement of a Safety phase is the failure mechanism and has no physical magnitude.)
- Fixed units, stated in every docstring: kN, m, day (seconds in dynamic phases).
- Handles, not names. What a call returns is what a phase activates.
- One contract.
prj.save("job.k2d")writes the same project the command line and the desktop application read — byte-identical to a hand-written file, pinned by the test suite. - Arrays out. Results are NumPy data, not screenshots.
import katai
# Units everywhere: kN, m, day.
prj = katai.Project("Strip footing", mesh_size=1.0, auto_refine=False)
clay = prj.materials.mohr_coulomb("Stiff clay", E=2.0e4, nu=0.3,
c=10.0, phi=25.0, gamma=18.0)
prj.geometry.rectangle(0, 0, 20, 10, material=clay)
load = prj.loads.line_load((8, 10), (12, 10), qy=-100.0) # 100 kPa over 4 m
prj.initial(procedure="k0", exclude=[load]) # ground first, load later
prj.phases.plastic("Load the footing", activate=[load])
prj.phases.safety("Factor of safety under load")
job = prj.run()
for res in job.results():
print(res.ok, f"max|u| = {res.max_disp:.4f} m", f"FoS = {res.fos:.3f}")
# True max|u| = 0.0000 m FoS = -1.000 <- K0 state: no displacement
# True max|u| = 0.0247 m FoS = -1.000 <- 24.7 mm under the footing
# True max|u| = 18.8336 m FoS = 1.780 <- factor of safety 1.78 The surface
Ten namespaces, at a glance.
| Namespace | What it makes |
|---|---|
prj.materials | linear_elastic · mohr_coulomb · hardening_soil · hs_small · soft_soil · soft_soil_creep · hoek_brown |
prj.geometry | polygon(points, material=, fix=[…], flow=[…]) · rectangle(x0, y0, x1, y1, material=) |
prj.water | table(y) · phreatic_line(points) |
prj.loads | line_load(a, b, qx=, qy=) · point_load(at, qx=, qy=) |
prj.structures | plate · anchor · geogrid · pile · interface |
prj.displacements | line(a, b, ux=, uy=) — prescribed displacement |
prj.dewatering | well(a, b, q=, h_min=) · drain(a, b, head=) |
prj.phases | plastic · consolidation · safety · transient_flow · fully_coupled · dynamic |
prj | initial(procedure=) · validate() · build() · save(path) · run() |
katai | summary(job, prj) · extremes(result) · Refusal · run(project) |
Stiffnesses are per metre out of plane, as everywhere in plane strain. Anchors and pile
rows are the exception and say so: they are discrete members, so they carry a
spacing in metres and the engine divides by it.
Reference
Materials, structures, phases, results.
Signatures checked against the source of release 0.9.0. Parameter values are illustrative.
Materials
prj.materials.linear_elastic("Concrete", E=3.0e7, nu=0.2, gamma=24.0,
drainage="non_porous")
prj.materials.mohr_coulomb("Sand", E=3.0e4, nu=0.3, c=1.0, phi=33.0, psi=3.0,
gamma=18.0, gamma_sat=20.0, k=1.0)
prj.materials.hardening_soil("Dense sand", E50=4.0e4, Eoed=4.0e4, Eur=1.2e5,
m=0.5, c=1.0, phi=35.0, psi=5.0, gamma=19.0)
prj.materials.hs_small("Stiff clay", E50=3.0e4, Eoed=3.0e4, Eur=9.0e4,
G0=1.875e5, gamma07=1.5e-4, c=10.0, phi=28.0, gamma=20.0)
prj.materials.soft_soil("Soft clay", lam_star=0.10, kap_star=0.02,
c=1.0, phi=25.0, gamma=16.0, drainage="undrained_a", k=1.0e-4)
prj.materials.soft_soil_creep("Peat", lam_star=0.15, kap_star=0.03, mu_star=0.006,
c=2.0, phi=20.0, gamma=12.0, drainage="undrained_a")
prj.materials.hoek_brown("Phyllite", E=7.5e6, nu=0.25, sigma_ci=50.0e3,
mi=7.0, gsi=48.0, D=0.0)
# drainage: "drained" | "undrained_a" | "undrained_b" | "undrained_c" | "non_porous"What each model computes, with its equations: constitutive models.
Structures
wall = prj.structures.plate((12, 20), (12, 6), EA=1.2e7, EI=1.0e5, w=5.0,
Mp=0.0, Np=0.0, interfaces="both", name="Wall")
anch = prj.structures.anchor((12, 18), (20, 14), EA=2.0e5, spacing=2.5,
prestress=300.0, Fmax_tens=0.0, name="Anchor")
grid = prj.structures.geogrid((4, 6), (20, 6), EA=1.0e4, Np=50.0)
pile = prj.structures.pile((12, 20), (12, 6), E=3.0e7, diameter=0.6, spacing=2.5,
Tskin_max=0.0, Fmax_base=0.0, connection="hinged")
slip = prj.structures.interface((4, 6), (20, 6))
# interfaces: "both" | "positive" | "negative" | "none"
# Mp, Np, Fmax_*, Tskin_max = 0 means UNLIMITED, not zero capacity.Phases
prj.initial(procedure="k0", exclude=[anch]) # "k0" | "gravity" | "safety"
prj.phases.plastic("Excavate", activate=[anch], deactivate=[pit])
prj.phases.plastic("Half the stage", apply_fraction=0.5) # Sum-Mstage
prj.phases.plastic("Design check", design="ec7_da3") # partial factors
prj.phases.plastic("Dewater", water=8.0) # phase water level
prj.phases.consolidation("Wait a month", duration=30.0, steps=100)
prj.phases.consolidation("Until 90 %", until_degree=90) # reports its time
prj.phases.consolidation("Until 1 kPa", until_excess_pore=1.0)
prj.phases.safety("FoS")
prj.phases.transient_flow("Drawdown", duration=10.0, steps=10)
prj.phases.fully_coupled("Rapid drawdown", duration=10.0, steps=10)
prj.phases.dynamic("Quake", duration=20.0, steps=2000, wave="record",
record=accel, record_dt=0.02, damping=0.05,
rayleigh=(1.0, 10.0), compliant_base=True, free_field=True)
# every phase also takes: tolerance=, load_steps=, max_iterations=,
# ignore_undrained=, reset_small_strain=
# design: "none" | "ec7_da1_c1" | "ec7_da1_c2" | "ec7_da2" | "ec7_da3"
# | "tbdy2018_static" | "tbdy2018_seismic"
Activation is inherited: a phase lists only what changes. until_degree is a
pressure ratio — the largest excess pore pressure left over the largest the stage
generated — not the settlement ratio; the two reach 90% at different times.
Running and reading results
job = prj.run(on_phase=lambda i, n, name: print(f" [{i}/{n}] {name}"))
print(katai.summary(job, prj)) # phases, extremes WITH locations, forces, diagnostics
res = job.results()[-1] # one result object per phase
res.ok, res.fos, res.max_disp, res.diagnostics
res.node_x, res.displacement, res.reaction # NumPy arrays
wall_f = next(s for s in res.struct_forces if s.name == "Wall")
wall_f.max_N, wall_f.max_Q, wall_f.max_M, wall_f.yielded
for st in wall_f.stations: # along the member
print(st.s, st.x, st.y, st.N, st.Q, st.M)A summary that says where
“The largest settlement is 43 mm” and “…and it is three metres from the wall” are two
different findings. katai.summary() gives both; a field that is uniform
says so instead of inventing a location. The block on the right is the output of the
published 0.9.0 wheel for the anchored excavation example (some rows omitted,
every value verbatim).
A refusal is an exception
When the engine will not solve a model — an input contract is broken, an object would
contribute nothing, an analysis is not defined for a material — it raises
katai.Refusal, carrying the message verbatim and the stable
K2D-* codes. A script branches on the reason instead of parsing prose.
try:
job = prj.run()
except katai.Refusal as e:
if any(d.code == "K2D-G003" for d in e.diagnostics):
... # a distributed load was drawn off the soil
print(e.report) # field-path issues, when the refusal came from validation=== Excavate to formation =======================================
status converged
max |u| 4.329029e-02 m
diagnostics
NOTE K2D-A004
A structural element ends on a supported node. The reported reactions
are the soil's contribution only; the element's own end force at that
support is not included in this build.
extremes min at max at
|u| [ m] 0 (0, 16) 0.04329 (3, 12)
u_x [ m] -0.0067762 (11.3, 5.9) 0.0065545 (1, 11)
u_y [ m] -0.00089196 (30, 18) 0.043216 (3, 12)
sig'_yy [kPa] -376.15 (30, 0) 5.888 (10, 12)
tau_max [kPa] 0 (0, 16) 120.34 (0, 11)
structural forces (envelope |max|)
Anchor row 1 anchor N 195.4 kN/m
Diaphragm wall plate N 325.7 kN/m Q 84.65 kN/m M 58.33 kNm/m Examples
Four complete programs, each checked by the test suite.
They ship under python/examples/ and run in
CTest against their verification bands — an example that stops telling the truth fails
the build rather than the first reader who tries it.
| Example | What it does | What it prints |
|---|---|---|
slope_stability.py | Griffiths & Lane (1999) slope by strength reduction | FoS 1.010; published ≈ 0.99 |
consolidation_terzaghi.py | Terzaghi column, by time and by target degree | |ΔU| ≤ 0.0061 at the sampled times |
staged_excavation.py | Staged excavation against 1D elastic unloading by hand | heave 30.31 mm vs 30.31 mm · +0.00% |
anchored_excavation.py | Diaphragm wall, interfaces, prestressed anchor | wall |M| 58.3 kNm/m; anchor 195.4 kN/m |
Requirements
- Python
- CPython 3.12 or newer — one abi3 wheel serves every later version.
- Platform
- Windows x64.
- Dependencies
- NumPy, installed automatically.
- Install
pip install katai2d-0.9.0-cp312-abi3-win_amd64.whl, downloaded from the releases page. See install.- Version check
python -c "import katai; print(katai.__version__)"→0.9.0
Writing code with an AI assistant? /llms.txt carries this API surface in a form written for language models, including which names need which release.
pip install, run an example, compare with the closed form.
KATAI 2D is free under Apache‑2.0, installs in one line and ships the input files of the benchmarks it is verified against. The fastest way to judge it is to rerun one.