Design Mode
Where a sweep asks how much does FS move, a design study asks the inverse — what value
of this parameter gives FS = 1.5? design() runs a fixed number of evenly spaced solves
across an explicit [low, high] range and linearly interpolates the parameter value where the
FS curve crosses the target. It is the deterministic-design staple: "vary the undrained
strength between X and Y and find where FS reaches the design factor." It shares the same
parameter grammar as Sensitivity — see
Addressing a parameter — and the same three
engine modes: mode='lem' (the default) and
mode='fem' both target a factor of safety, while mode='seep' sweeps the seepage problem
and targets the total discharge q instead — target_fs and the crossing/bracketing logic
below apply identically, just against q rather than FS.
In XSLOPE Studio this runs behind the Parametric… button (its Design mode) — see Studio: Parametric study for the dialog.
Running a design study
from xslope.sensitivity import design
from xslope.plot import plot_sensitivity
success, result = design(
slope_data,
param="mat:Soil:c", # or {"material": "Soil", "property": "c"}, or a tuple
low=6, high=18, steps=7, # 7 evenly spaced solves across [6, 18]
target_fs=1.5,
method="bishop", # one method — a design study locates a single curve
num_slices=30,
)
print(result['message'])
plot_sensitivity(result['df'], target_fs=result['target_fs'])
design() returns (success, result); on success result carries the sweep DataFrame
(result['df'], exactly the sensitivity shape above) plus a summary:
| field | meaning |
|---|---|
crossing |
interpolated parameter value at FS = target_fs, or None if the target is not reached |
crossings |
every crossing found — a non-monotonic curve can cross twice; crossing is the first |
bracketed |
True only when the target is crossed inside [low, high] |
fs_range |
(min FS, max FS) over the successful sweep points |
direction |
'increasing' / 'decreasing' / 'non-monotonic' trend of FS vs the parameter |
extend |
on a miss, 'above {high}' or 'below {low}' — which way to widen the range; else None |
message |
one-line human-readable summary |
param, target_fs, base_value, runtime |
canonical ref, the target, the parameter's current value, wall-clock seconds |
design() also takes progress_callback and cancel_check hooks (a per-point progress
callback and a cooperative cancel), which is how Studio streams a progress bar and a Cancel
button over a background sweep; a plain data-in/data-out call leaves both None.
Sweeping anything else: modify=
design() shares the sensitivity engine's escape hatch, so the quantity you design need not
be a single stored scalar. When it is geometry — a slope angle, a berm width — pass a
callable in place of a parameter reference: modify is a (slope_data, value) -> slope_data
setter and label names the swept axis, and the two are mutually exclusive with param
(exactly one per call). low/high/steps sweep the callable's value the same way, and the
crossing, bracketing, and extend/direction reporting are identical. The callable owns
whatever consistency its edit requires — here, rebuilding the material polygons and ground
surface after it moves a profile point, since slice weights come from the polygons, not the
raw profile lines:
import math
from shapely.geometry import Polygon
from xslope.fileio import build_ground_surface_from_polygons
from xslope.mesh import build_polygons
def set_slope_angle(sd, beta_deg):
"""Rotate the slope face about the toe to beta_deg, then rebuild the polygon
geometry (see the Slope Design page for this rebuild pattern)."""
prof = sd['profile_lines'][0]['coords']
x_toe, y_toe = prof[1]
_, y_top = prof[2]
prof[2] = (x_toe + (y_top - y_toe) / math.tan(math.radians(beta_deg)), y_top)
polys = [{'polygon': Polygon(p['coords']), 'mat_id': p['mat_id']}
for p in build_polygons(slope_data={'profile_lines': sd['profile_lines'],
'max_depth': sd.get('max_depth')})]
sd['polygons'] = polys
sd['ground_surface'], sd['domain_polygon'] = build_ground_surface_from_polygons(polys)
return sd
success, result = design(
slope_data, modify=set_slope_angle, label="slope angle (deg)",
low=12, high=25, steps=8, # flatten the face from 25° toward 12°...
target_fs=1.5, method="bishop", # ...and find the angle that reaches FS = 1.5
)
print(result['message'])
# FS = 1.5 at slope angle (deg) = 16.74 (interpolated between solves).
On the shipped ACADS slope this brackets the target — FS climbs from about 1.04 at 25° to
2.02 at 12° as the face flattens, crossing FS = 1.5 at a slope angle of 16.74°. A
built-in param reference and a modify= callable are one code path — every reference
resolves internally to exactly the setter signature modify= takes — so the answer is
identical whichever way you name the swept axis, and the engine validates the modified model
at each step (polygon validity, ground surface present) precisely because a setter may be
user-written: a broken edit becomes an honest success=False sweep point, never a silently
inconsistent crossing.
Honest about misses
The engine never extrapolates. A crossing is reported only when
the target is bracketed by two actual solves. If the swept range never reaches the target,
bracketed is False, crossing is None, and extend names the direction to widen the
range — the study reports that it fell short rather than projecting a value past the last
solve.
Worked example
On the ACADS simple slope used throughout these pages (base cohesion c = 3 kPa, base Bishop FS = 0.985), sweeping the cohesion from 6 to 18 kPa in seven steps and asking where FS reaches 1.5:
FS = 1.5 at mat:Soil:c = 13.4 (interpolated between solves).
result['crossing'] is 13.40, result['bracketed'] is True, and result['direction'] is
'increasing'; the sweep spans FS = 1.154 (c = 6) to FS = 1.693 (c = 18). Re-solving at the
interpolated c = 13.40 returns FS = 1.500 — the linear interpolation between the c = 12 and
c = 14 solves lands the target to within 0.01%.
Ask the same question over a range that never reaches the target — say c from 3 to 9 — and the study declines to guess:
FS = 1.5 is not reached for mat:Soil:c in [3, 9] — FS spans [0.985, 1.302].
Extend the range above 9 to bracket it.
Here result['bracketed'] is False, result['crossing'] is None, and result['extend']
is 'above 9'.