Dynamic Stereonet Tab

Located at dynamic_analyzer/tabs/dynamic_stereonet_tab.py, the Stereonet Tab (implemented as the mixin class StereonetTab) is one of six coordinated views inside the Dynamic Analyzer workspace (the others being Map, Rose, Mohr, Table, and Method). It renders a lower-hemisphere, equal-area (Schmidt) projection of every fracture plane currently loaded into the workspace, plotted as poles (points representing the plane normal). Superimposed on the same projection is a continuous colour field showing how a chosen stress-derived attribute — slip tendency, dilation tendency, fracture susceptibility, or a four-tier critical-fracture-state classification — would vary across every conceivable plane orientation for the stress tensor currently defined in the sidebar. The combination of discrete, data-derived poles and a continuous theoretical field is what makes the Stereonet Tab a genuinely predictive tool rather than a simple plotting utility: it lets the analyst see at a glance whether the orientations actually present in the fractured rock mass coincide with the orientations that the applied or inferred stress field would drive toward failure.

Within the broader FracSTAT architecture, the Stereonet Tab consumes the same self.results DataFrame that feeds the Map, Rose, Mohr, and Table tabs, and it shares the same stress-resolution outputs (SlipTendencyNorm, DilationTendency, FractureSusceptibility, CriticalStateValue) produced by calculator.run_calculation(). It therefore requires no independent data entry: any SVG trace map or Excel/CSV orientation table already loaded for the Map tab is automatically available here. The module's scope is strictly the stereographic (angular) domain — it does not compute spatial statistics (that is the Rose tab's role) or the Mohr–Coulomb failure-envelope diagram (the Mohr tab's role); rather, it is the 3-D orientation counterpart to the 2-D Mohr diagram, showing where in orientation space the fractures sit relative to the full field of possible responses.

Position within FracSTAT

  • Static Analyzer — Stereonet Plot tab: purely geometric (strike/dip only), no stress resolution.
  • Dynamic Analyzer — Stereonet Tab (this module): stress-informed, colour-coded by the active tendency attribute.
  • Dynamic Analyzer — Mohr Tab: the 2-D shear/normal-stress analogue of this 3-D orientation view; both tabs share the same four colour-by metrics and the same selection state.
  • Dynamic Analyzer — Map Tab: spatial view of the same segments; clicking a pole here highlights the same segment on the Map.
Overview of the Stereonet Tab layout
Figure 1.1 — The Stereonet Tab within the Dynamic Analyzer workspace, showing the header tile row, the main stereographic plot with coloured tendency field and fracture poles, and the right-hand Pole Inspector / Settings panel.

1. Interface Overview

The tab is organised as a two-column layout inside the Dynamic Analyzer's tabbed workspace. The left (main) column is a rounded card containing a header, a row of three summary tiles, and the plotting canvas. The right column is a scrollable panel containing the Pole Inspector, the dip-override loader, a usage tip, the Colour Metric selector, and the full Settings section.

1.1 Header and Summary Tiles

The header carries the title "Stereonet", a one-line description ("Lower-hemisphere equal-area projection of fracture poles and tendency field"), a colour-scale dropdown (shared with the Mohr tab, see Section 5.1), and a Save Image button. Beneath the header, three tiles summarise, at a glance, the current state of the plot:

TileDisplayed ValueUpdate Trigger
Fracture polesCount of segments/planes currently plotted (len(self.results))Every redraw of the stereonet
Active fieldName of the currently selected colour-by attribute (e.g. "Slip tendency")Whenever the Mohr/Stereonet shared metric checkboxes change
Colour scaleName of the selected Matplotlib colormap (e.g. "Plasma")Whenever the colormap dropdown changes

1.2 Main Plot Canvas

The plot canvas is a single square Matplotlib Axes hosting: the equal-area primitive circle, optional grid rings/degree ticks, the N/E/S/W (or numeric) perimeter labels, the continuous tendency field (a filled Delaunay triangulation), the fracture poles (a scatter collection with picking enabled), optional great-circle planes, the three principal-stress markers, a draggable "Symbols" legend box, and — when a colour metric is active — a colour bar. A dedicated horizontal Axes beneath the main plot hosts the colour bar so that its position and thickness can be adjusted independently of the main circle's aspect ratio.

1.3 Pole Inspector Panel

The right panel opens with a short instructional paragraph ("Click a pole to inspect the resolved stress for that fracture.") followed by a bordered card labelled SELECTED FRACTURE. This card is blank ("No pole selected yet.") until the user clicks a pole or a row in the Table tab; it is populated by update_stereonet_selection_panel(), described in Section 3.6.

1.4 Navigation and Selection Controls

ControlTypeAction
Show in MapButtonSwitches the workspace to the Map tab with the same segment ID highlighted (select_segment(..., switch_to_map=True)).
Clear SelectionButtonDeselects the current pole across Map, Mohr, Table, and Stereonet simultaneously (clear_selection()).
Dip Override cardComposite widgetShared control (built by _build_dip_override_card) for loading a CSV/Excel dip-override file; described in Section 5.6.

2. Data Input and Preprocessing

The Stereonet Tab performs no file I/O of its own. All orientation and stress data arrive pre-computed in self.results, a pandas DataFrame produced by calculator.run_calculation() once the user has loaded an SVG trace map or an Excel/CSV orientation table and pressed Calculate (or edited any stress parameter). The columns the Stereonet Tab reads are summarised below.

ColumnSourceRole in Stereonet Tab
StrikeDegSVG geometry or preset Excel strikeConverted to pole trend (Section 3.2); read modulo 360° to preserve dip direction.
DipDegSVG default dip / Excel preset dip / per-ID overrideConverted to pole plunge via the equal-area radius formula (Section 3.1).
IDRow index assigned in run_calculation()Used for pole picking (on_stereonet_pick) and cross-tab selection.
SlipTendencyNormWallace–Bott resolved shear/normal ratio, normalised by the theoretical maximumOne of four selectable colour-by metrics.
DilationTendency(σ1 − σn)/(σ1 − σ3)One of four selectable colour-by metrics.
FractureSusceptibilityσn − Pf − (τ − C0)/μOne of four selectable colour-by metrics.
CriticalStateValueFour-tier friction-demand classification (0–3)One of four selectable colour-by metrics; forces a discrete four-colour scale.
EffectiveSigmaN, TauResolved normal/shear stress at the current stress tensorDisplayed verbatim in the Pole Inspector card.

2.1 Dip Override Injection

Before strikes and dips reach the plotting routine, the tab checks self.results for an ID column and a non-empty static/dynamic dip_overrides mapping (populated by the shared Load Dip Override control, see Section 5.6). Where an override exists for a given ID, the DipDeg value is replaced; rows without an override keep their original dip. Because the dip determines the pole's radial position on the equal-area net, this override changes where — but not whether — the corresponding pole is drawn.

2.2 Dip-Direction Correction

Legacy Excel imports and any workflow that supplies a DipDirection column trigger an additional correction: wherever DipDirection is finite, the strike used for pole placement is recomputed as (DipDirection − 90°) mod 360°, overriding the raw StrikeDeg value for that row only. This keeps the stereonet consistent with data sources that report dip azimuth directly rather than strike.

2.3 Validity Filtering

Rows with non-finite strike or dip (e.g. produced by malformed input rows that survived upstream validation) are excluded from the plotted set via a combined finite-value mask; dip is additionally clipped to the physically valid 0–90° range before any trigonometric evaluation. If the filtered set is empty, the tab falls back to an explanatory empty state (Section 6.4) rather than attempting to draw.

3. Mathematical Background

3.1 Lower-Hemisphere Equal-Area (Schmidt) Projection

Every point plotted on the net — poles, principal-stress axes, and great-circle traces of planes — uses the Lambert equal-area, lower-hemisphere convention standard in structural geology (Ragan, 2009). For a line with trend θ (measured clockwise from north) and plunge δ (measured downward from horizontal), the projected radius r and Cartesian plotting coordinates (x, y) are:

r = √2 · sin( (90° − δ) / 2 ) x = r · sin(θ), y = r · cos(θ)

The primitive circle (the outer boundary of the net) corresponds to r = 1, i.e. a horizontal line (δ = 0°); the centre of the net corresponds to a vertical line (δ = 90°). This projection preserves area, which is what permits density-style visual comparison between clusters of poles without the areal distortion of a stereographic (equal-angle) projection (Vollmer, 1995; Cardozo and Allmendinger, 2013).

3.2 Pole Position from Strike and Dip

A fracture plane is fully described by its strike (azimuth of the horizontal line contained in the plane, right-hand-rule convention) and dip (angle below horizontal, measured perpendicular to strike). Its pole is the line perpendicular to the plane, and by the right-hand rule its trend is exactly 90° counter-clockwise from the dip-direction, i.e. (Allmendinger et al., 2012):

pole_trend = (strike − 90°) mod 360° pole_plunge = 90° − dip

Substituting pole_plunge into the equal-area radius formula from Section 3.1 gives the specific expression used throughout the module for the pole radius, r = √2 · sin(dip/2), since 90° − pole_plunge = dip. The module deliberately preserves the fully-directed 0–360° form of strike (rather than folding it into the 0–180° fracture convention used by the Rose tab) precisely because the pole position depends on which side of the strike line the plane dips toward; folding the strike would silently collapse two physically distinct dip directions onto a single hemisphere position.

Note: the module uses the full 0–360° strike (mod 360) rather than trusting whatever range StrikeDeg happens to already be in. Folding strike to 0–180° upstream throws away which side the plane dips toward — exactly what would collapse poles onto a single hemisphere instead of the two clusters a correct stereonet should show for the same data.

3.3 Great-Circle (Plane) Trace

When the "Show all planes" or "Show selected plane only" toggle is enabled, each plane's full great-circle trace is drawn by sweeping a parameter t from 0 to π across the strike and down-dip unit vectors in an East-North-Up basis:

v(t) = strike_vector · cos(t) + down_dip_vector · sin(t)

where strike_vector = (sinψ, cosψ, 0), and down_dip_vector = (sin(ψ+90°)cosδ′, cos(ψ+90°)cosδ′, −sinδ′) for strike azimuth ψ and dip δ′. Each swept vector v(t) = (vE, vN, vU) is converted back to a trend/plunge pair via plunge = asin(−vU) and trend = atan2(vE, vN), then projected with the same equal-area radius formula, producing the smooth curved trace characteristic of a dipping plane on an equal-area net (a vertical plane traces a straight diameter; a shallow plane traces a tight loop near the primitive circle).

3.4 Stress Resolution on the Continuous Orientation Field

The coloured field underlying the poles is not derived from the loaded fracture data at all — it is the theoretical response of the current stress tensor evaluated across a dense, regular grid of colatitude and trend values covering the entire lower hemisphere (64 colatitude steps × 128 trend steps, avoiding the exact pole and equator to prevent singularities). For each grid orientation the unit normal vector components in an East/North/Up basis are:

nE = sin(colat)·sin(trend) nN = sin(colat)·cos(trend) nU = −cos(colat)

These are projected onto the three principal-stress axis directions (Section 3.5) to obtain direction cosines c0, c1, c2, from which the resolved normal and shear stress follow the standard Cauchy stress-transformation identities (Jaeger et al., 2007):

σn = σ1·c0² + σ2·c1² + σ3·c2² τ = √[ σ1²c0² + σ2²c1² + σ3²c2² − σn² ]

From these, the four selectable field values are computed grid-point-by-grid-point exactly as in the Mohr tab:

MetricFormulaNormalisation
Slip tendency (normalised)Ts = τ / max(σn, 10⁻¹²)Divided by the closed-form theoretical maximum Ts,max = (σ1 − σ3) / (2·√(σ1·σ3)), not by the largest value on the grid, so the field matches the per-fracture SlipTendencyNorm column exactly (Section 7.1).
Dilation tendencyTd = (σ1 − σn) / (σ1 − σ3)Already dimensionless, 0–1 by construction.
Fracture susceptibilitySf = σn − Pf − (τ − C0)/μRaw form displayed in stress units (e.g. MPa). The normalised form is rescaled against the loaded fractures' Sf minimum and maximum and then inverted, 1 − (Sf − min)/(max − min), so the least stable orientations map to 1.0, matching the inversion in calculator.py; the field is set to 0 everywhere if that range collapses below 10⁻¹².
Critical fracturesthreshold_ratio = [(τ − C₀) / (σn − Pf)] / μ, banded to 0=Stable, 1=Near Critical, 2=Critical, 3=Unstable at the 50%/75%/100% thresholdsDiscrete four-colour scale (Stable / Near Critical / Critical / Unstable).

This same field-evaluation logic is shared with the Mohr tab's own contour computation, ensuring the two diagrams are always mutually consistent for a given stress state — a fracture that appears deep in the "hot" colour region on the stereonet will also plot on or beyond the Coulomb envelope in the Mohr diagram.

3.5 Principal Stress Axis Construction

Two stress-orientation modes are supported, matching the Mohr and Map tabs. In Andersonian mode (Anderson, 1951), σ1 is horizontal along the entered azimuth, σ2 is exactly vertical, and σ3 is horizontal and perpendicular to σ1:

σ1_axis = (sinα, cosα, 0) σ2_axis = (0, 0, 1) σ3_axis = (cosα, −sinα, 0)

where α is the σ1 azimuth in radians. This is the strike-slip Andersonian regime (Sv = σ2) — the only one the simplified mode builds, and now named explicitly as the default vertical_axis="sigma2" of andersonian_principal_axes(), which also accepts "sigma1" (normal faulting, Sv = σ1) and "sigma3" (reverse/thrust, Sv = σ3). Note that the Uncertainty Analyzer's default is the opposite convention, Sv = σ3, so a stereonet built here and a slip-tendency distribution built there are not directly comparable unless the same regime is chosen in both. In General 3-D mode, the fully general routine principal_stress_axes_fracstat(σ1_trend, σ1_plunge, σ3_trend) is used: σ1 is built directly from its own trend/plunge; σ3's plunge is solved so that σ3 is exactly orthogonal to σ1 (using the supplied σ3 trend as a starting horizontal direction and re-normalising); and σ2 is obtained as the cross product σ3 × σ1, ensuring a mutually orthogonal right-handed frame from only three independent input angles. For plotting purposes, axis_to_trend_plunge() converts each resulting axis back into a trend/plunge pair using a downward-pointing-line convention with a tolerance-based sign correction (±10⁻⁹) that guards against floating-point noise spuriously flipping a near-horizontal axis by 180°.

3.6 Kinematic (Wallace–Bott) Consistency for Focal Mechanisms

The Wallace–Bott hypothesis holds that slip on a pre-existing plane occurs parallel to the resolved shear traction on that plane (Wallace, 1951; Bott, 1959), so the angular difference between the observed and predicted slip directions is a direct test of whether an observed slip event is compatible with the assumed stress tensor. When a loaded dataset is a focal-mechanism (earthquake) file supplying a rake/slip value, the Stereonet Tab shares the same slip-tendency, dilation-tendency, and fracture-susceptibility metrics computed for the nodal plane; the underlying calculator additionally resolves the shear traction along the observed slip vector and reports a Rake Misfit, but this Wallace–Bott diagnostic is displayed in the Pole Inspector text panel (shared with the Map and Mohr tabs) rather than as a stereonet colour metric or a Table-tab column: the inspector prints the signed resolved shear along the slip vector, the predicted rake, and the misfit, labelling the latter "consistent" at ≤ 30° and "inconsistent" above it.

Sign conventions in the rake diagnostic. Both quantities are built from the fault-plane basis returned by fault_basis_vectors(strike, dip) in an (East, North, Up) frame: the unit strike vector ŝ = (sin strike, cos strike, 0), the down-dip vector d̂ = (sin·cos, cos·cos, −sin) taken along the dip direction strike + 90°, and the upward pole n. Because the traction is assembled from compression-positive stress magnitudes, the physical (tension-positive) shear traction acting on the hanging wall is the negative of the in-plane traction component, and the observed slip direction follows the Aki and Richards (1980, §4.2) rake convention (0° left-lateral, +90° reverse/up-dip, 180° right-lateral, −90° normal/down-dip):
shear_traction = −( T − σn·n ) slip_vector = cos(rake)·ŝ − sin(rake)·d̂ (equivalently cos(rake)·ŝ + sin(rake)·û, û = up-dip) τ_rake = shear_traction · slip_vector (signed driving stress along the observed slip) predicted_rake = atan2( −(shear_traction · d̂), shear_traction · ŝ ) Rake Misfit = | (predicted_rake − rake + 180°) mod 360° − 180° |
The two minus signs are not cosmetic. τ_rake is a signed quantity — positive when the resolved shear drives slip in the observed direction, negative when the stress field would drive the opposite sense — so getting the traction sign wrong inverts the geological interpretation of every focal mechanism while leaving the unsigned τ and Ts untouched. Likewise the minus sign on the dip component of predicted_rake converts a down-dip component into the up-dip-positive Aki and Richards (1980) sense, so that observed and predicted rakes are compared in the same convention; and the modulo-360 wrap folds the difference into 0–180°, so a misfit is never reported as, say, 350° when the true angular separation is 10°.
python
colatitude = np.linspace(.01, np.pi / 2 - .01, 64)
trend = np.linspace(0, 2 * np.pi, 128, endpoint=False)
colatitude, trend = np.meshgrid(colatitude, trend)

normal_east  = np.sin(colatitude) * np.sin(trend)
normal_north = np.sin(colatitude) * np.cos(trend)
normal_up    = -np.cos(colatitude)
s1, s2, s3 = stress["sigma1"], stress["sigma2"], stress.get("sigma3", stress["sigma2"])

sigma_n = s1*c0**2 + s2*c1**2 + s3*c2**2
traction_sq = s1**2*c0**2 + s2**2*c1**2 + s3**2*c2**2
tau = np.sqrt(np.maximum(traction_sq - sigma_n**2, 0.0))

if column.startswith("SlipTendency"):
    raw = tau / np.maximum(sigma_n, 1e-12)
    if column == "SlipTendencyNorm":
        # Closed-form theoretical maximum, NOT raw.max() over the grid.
        denom = 2.0 * np.sqrt(max(s1 * s3, 1e-12))
        tsmax = max((s1 - s3) / denom, 1e-12)
        field_values = raw / tsmax
    else:
        field_values = raw
elif column == "DilationTendency":
    field_values = (s1 - sigma_n) / max(s1 - s3, 1e-12)
elif column.startswith("FractureSusceptibility"):
    raw_values = sigma_n - stress["pore_pressure"] - (tau - stress["cohesion"]) / max(stress["friction"], 1e-12)
    if column == "FractureSusceptibilityNorm" and not self.results.empty:
        sf_min = float(self.results["FractureSusceptibility"].min())
        sf_max = float(self.results["FractureSusceptibility"].max())
        sf_range = sf_max - sf_min
        # Inverted: a SMALLER Sf is MORE susceptible, so it maps to 1.0.
        field_values = (np.zeros_like(raw_values) if abs(sf_range) < 1e-12
                        else 1.0 - (raw_values - sf_min) / sf_range)
    else:
        field_values = raw_values
else:
    field_values = (tau >= stress["cohesion"] + stress["friction"]*(sigma_n - stress["pore_pressure"])).astype(float)

radius = np.sqrt(2) * np.sin(colatitude / 2)
field_x, field_y = (radius*np.sin(trend)).ravel(), (radius*np.cos(trend)).ravel()

Simplified excerpt of _draw_stereonet(), showing the analytic 64×128 grid construction and the four selectable field-value formulas.

4. Algorithms

4.1 Field-Contouring Algorithm

  1. Build a meshgrid of 64 colatitude values (0.01 to π/2 − 0.01) by 128 trend values (0 to 2π, endpoint excluded).
  2. Compute the unit normal-vector components at every grid node (Section 3.4).
  3. Project each normal onto the active principal-stress frame to obtain direction cosines.
  4. Evaluate σn and τ, then the selected scalar field value (Ts, Td, Sf, or the four-tier critical-fracture-state classification).
  5. Convert every grid node's (colatitude, trend) into equal-area (x, y) plotting coordinates using the same radius formula as the poles.
  6. Triangulate the resulting scattered point cloud with matplotlib.tri.Triangulation and render it with ax.tricontourf at 24 discrete colour levels (or, for the Critical fractures metric, at the fixed boundary levels [-0.5, 0.5, 1.5, 2.5, 3.5] so the four states render as crisp bands rather than a smoothed gradient).

Because the triangulation is built once per redraw directly from the analytic grid (not from the sparse, irregularly spaced fracture poles), the field is smooth and continuous regardless of how many or how few fractures are actually loaded — it represents the theoretical stress response, not an interpolation of the data.

4.2 Pole Placement and Picking

Poles are computed vectorised across the entire filtered DataFrame using NumPy: pole_trend = (strike − 90°) mod 360° and pole_radius = √2·sin(dip_rad/2), then x = radius·sin(trend_rad), y = radius·cos(trend_rad). The resulting scatter artist is registered as a matplotlib "pickable" object with a pick radius of 6 points; a click within that radius fires on_stereonet_pick, which maps the picked index back to a row of self.results and calls the shared select_segment() method — the same selection entry point used by the Map, Mohr, and Table tabs, guaranteeing the four views never fall out of sync.

4.3 Draggable Symbol Index

The legend ("Symbols") box in the lower-right corner of the plot is not a native Matplotlib Legend but a hand-built composite of a FancyBboxPatch, text labels, and scatter markers, all scaled by the current global font size relative to a 10-pt baseline. This custom construction allows the box to be interactively repositioned when "Move Elements" mode is active in the workspace header: on_stereo_press hit-tests the box's stored (x0, y_top, width, height) footprint in axes-fraction coordinates; on_stereo_motion translates the anchor by the mouse displacement (expressed as a fraction of the axes' pixel bounding box) and calls a cheap redraw routine (_redraw_stereo_symbol_index) that repositions only the index artists rather than re-running the full stereonet draw, which would be considerably more expensive because it would re-evaluate the entire field-contouring grid. The anchor position persists across redraws in self._stereo_symbol_index_pos, and defaults to just outside the plot's own axes bounding box (x0 = 1.02) so it never overlaps the stereonet circle regardless of how large the font grows.

4.4 Colour-Bar Layout Algorithm

Depending on the selected Colour bar position (Bottom/Top/Left/Right/None), the routine computes a rectangle for the dedicated colour-bar Axes (in figure-fraction coordinates) sized by the user-configured thickness and length fractions, and simultaneously repositions the main stereonet Axes so the circle stays centred and undistorted next to the bar. The Diagram Scale slider (Section 5.3) is applied multiplicatively to both width and height of the main Axes about the same centre point used for the colour-bar layout, so enlarging or shrinking the diagram never displaces its centre.

4.5 Selected-Pole Highlight and Selected-Plane Trace

When self.selected_segment_id is not None, a three-layer halo (white ring, dark ring, coloured "P" plus-marker) is drawn over the corresponding pole and annotated with its ID. If "Show Selected Plane" is enabled, the great-circle trace of that one plane (Section 3.3) is additionally drawn in the user-selected highlight colour, letting the analyst see the full orientation of a single fracture of interest without cluttering the net with every plane in the dataset.

5. Settings and Controls

All settings are held in dedicated CTk variables, lazily initialised the first time the tab is built (_stereo_init_vars) so that re-opening the tab, switching projects, or restoring a saved session never silently resets a control the user had already configured. Every control below triggers _draw_stereonet() on change unless noted otherwise.

5.1 Colour Metric (shared with the Mohr Tab)

Four checkboxes — Slip tendency, Dilation tendency, Fracture susceptibility, Critical fractures — are mutually exclusive (selecting one automatically clears the others) and are bound to the same mohr_slip_var / mohr_dilation_var / mohr_fracture_var / mohr_critical_var variables used by the Mohr tab, via the shared change_mohr_metric() callback. This deliberate sharing means changing the active metric on either tab updates both simultaneously; the Stereonet Tab does not maintain its own independent copy of the "Critical fractures" state, so the four-tier classification and its thresholds are always identical between the two tabs for a given stress state.

Excel fracture poles over the theoretical dilation-tendency field on an equal-area stereonet
Figure 5.1 — Dilation-tendency field for the 21 Excel fracture planes, using principal stresses 100/50/25 MPa and the existing stress orientation; cohesion and pore pressure remain 0 MPa and friction is 0.6. Red poles are measured input orientations; the continuous background evaluates all possible plane orientations, not spatial interpolation between measurements. The cyan cross retains the selection of ID 17, and the white symbols identify the three principal-stress axes.

5.2 Settings Card — General Appearance

ControlWidgetDefaultEffect
Colour bar positionDropdownBottomBottom / Top / Left / Right / None; repositions colour bar and rescales the main diagram (Section 4.4).
Outer BGColour swatch#F8FAFCFigure (outer canvas) background colour.
Inner BGColour swatch#FFFFFFStereonet Axes background colour.
CBar WidthNumeric entry0.03Colour-bar thickness as a fraction of the figure.
CBar LengthNumeric entry0.64Colour-bar length as a fraction of the figure.

5.3 Settings Card — Pole and Border Styling

ControlWidgetDefaultEffect
Pole ColorColour swatch#FF0000Fill colour of pole markers when solid-colour mode effectively applies (no metric selected uses this colour for the index; poles themselves are always coloured by the active field when a metric is chosen).
Pole BorderColour swatch#FF0000Marker edge colour.
Border WidthNumeric entry0.3Marker edge line width, points.
Selected PoleColour swatch#00D4FFHighlight colour used for the selected pole's plus-marker and, if enabled, the selected plane's great-circle trace.
Boundary ColorColour swatch#111111Colour of the primitive circle and (if enabled) the 30° tick marks.
Boundary WidthNumeric entry1.2Line width of the primitive circle.
Pole ShapeDropdownCircleCircle / Diamond / Square / Triangle Up / Star / Cross — maps to Matplotlib marker codes o / D / s / ^ / * / X.
Pole SizeNumeric entry24Marker area in points² (matplotlib scatter s parameter).
Diagram ScaleNumeric entry1.0Multiplicative scale factor applied to both width and height of the main Axes about its current centre (Section 4.4).
Custom TitleText entry(blank)Overrides the auto-generated title "<metric> — fracture poles".

5.4 Settings Card — Toggles

ToggleDefaultEffect
Show N-S / E-W LinesOnDraws the two crosshair diameters through the net centre.
Show Selected PlaneOffDraws the great-circle trace of only the currently selected fracture, in the Selected Pole colour.
Show All PolesOnToggles visibility (and pickability) of the entire pole scatter collection; turning it off disables clicking to select a fracture from the stereonet.
Show All PlanesOffDraws every loaded fracture's great-circle trace at low opacity (alpha = 0.25), useful for visualising the overall fabric of dipping surfaces rather than just their poles.

5.5 Reset Defaults

The Reset Defaults button (_stereo_reset_defaults) restores every one of the settings above to its factory value in a single action and immediately redraws — useful after exploratory styling changes before generating a publication figure.

5.6 Dip Override Card

Shared with every other Dynamic Analyzer tab that displays per-segment dip (Mohr, Map, Stereonet), this card provides: a "Load Dip Override (CSV/Excel)" button that opens a file dialog and parses an ID/DipDeg table (column names are matched case-insensitively after stripping non-alphanumeric characters, so "Dip Deg", "dip_deg", and "DipDeg" are all accepted); a "File Format" button that opens a hover preview showing the exact two-column layout expected; and a "Clear Overrides" button. Overrides are validated to lie within 0–90° before being accepted, and are keyed by the same one-based ID used in the Table tab, so an override file exported from one project session can be reused directly against another session with an identical segment count and ordering.

6. Internal Workflow

6.1 Build Sequence (_build_stereonet_tab)

  1. Initialise all settings variables (Section 5) and the draggable symbol-index state (position, drag flag, artist list).
  2. Construct the outer rounded card, header row (title, subtitle, colour-scale dropdown, Save Image button), and the three summary tiles.
  3. Construct the plot card containing the Matplotlib Figure/Axes pair and its dedicated colour-bar Axes, then wrap it in a FigureCanvasTkAgg and bind pick_event, button_press_event, motion_notify_event, and button_release_event handlers.
  4. Construct the right-hand scrollable panel: Pole Inspector heading and instructions, the Selected Fracture card, Show in Map / Clear Selection buttons, the Dip Override card, a Tip card, the Colour Metric checkboxes, and the full Settings card tree.
  5. Call _draw_stereonet() once to render the initial (likely empty) state.

6.2 Draw Sequence (_draw_stereonet)

  1. Read and validate every styling variable (colours, widths, sizes), falling back to a safe default on any parse failure so a malformed numeric entry never crashes the redraw.
  2. Clear the Axes and colour-bar Axes; reset the fixed aspect-1 limits (−1.15 to 1.15 in both axes) and hide the default tick marks.
  3. Draw the primitive circle, optional crosshairs, and either the 30° perimeter tick/degree-label ring (when "Show Selected Plane" is on) or the plain N/E/S/W labels.
  4. If self.results is empty or no stress has been calculated, populate the summary tiles with placeholder text, render the title "Stereonet", and return early (Section 6.4).
  5. Resolve the active colour metric (falling back to Slip tendency if none is checked), update the three summary tiles, and build the principal-stress axis frame (Section 3.5).
  6. Evaluate the continuous tendency field over the analytic grid (Sections 3.4/4.1) and render it with tricontourf; redraw the primitive circle on top so the field never bleeds past the net's boundary.
  7. Compute pole_trend/pole_radius for every valid row (Section 4.2); optionally draw every plane's great circle first (so poles render on top), then draw the pole scatter (respecting "Show All Poles").
  8. If a segment is selected, draw its highlight halo, ID annotation, and (optionally) its individual great-circle trace.
  9. Attach the ScalarMappable-driven colour bar per the chosen position (Section 4.4), draw the three principal-stress axis symbols, and render the draggable Symbols index (Section 4.3).
  10. Apply the custom or auto-generated title and request an idle redraw of the canvas (draw_idle).

6.3 Selection Synchronisation

select_segment() (defined at the SlipTendencyApp level, not within this mixin) is the single choke point through which a pole click, a Table row click, or a Mohr-diagram click all flow. It updates self.selected_segment_id, calls update_stereonet_selection_panel() to refresh the Pole Inspector card, and then calls _draw_stereonet() (among the other tabs' own redraw methods) so the newly selected pole's halo appears immediately, without the user needing to manually refresh.

Selected Excel plane ID 17 and its cyan great circle beside the Stereonet Pole Inspector
Figure 6.1 — Linked selection of Excel plane ID 17 in the Stereonet. The cyan great circle traces the selected plane, while its highlighted pole marks the normal. The Pole Inspector reports strike 51.2°, dip 73.2°, effective normal stress 52.01 MPa, and shear stress 30.32 MPa—the same stress pair shown in the Mohr example. The background here is normalised slip tendency, rather than the dilation field in Figure 5.1.

6.4 Empty and Degenerate States

Two distinct "nothing to show" conditions are handled: (a) no data loaded at all, or no stress tensor yet calculated — the tiles reset to "0" / "Slip tendency" / the current colour-scale name, and the title reads simply "Stereonet"; and (b) data loaded but every row fails strike/dip validity filtering (Section 2.3) — handled by the same early-return path since the filtered array would again be empty. In both cases the primitive circle, crosshairs, and cardinal labels are still drawn, so the net's geometry frame of reference is always visible even with no data.

7. Statistical Methods

The Stereonet Tab itself performs no descriptive statistics beyond a simple count (the "Fracture poles" tile). All quantitative summary statistics for the loaded fracture population (mean strike, dominant azimuth bin, dispersion, etc.) belong to the Rose tab; the Stereonet Tab is instead the qualitative / visual complement, letting the analyst judge cluster tightness, bimodality, or great-circle girdle patterns by eye against the coloured stress field. Where quantitative rigor is required — e.g. testing whether a pole cluster is statistically consistent with the maximum-Ts orientation predicted by the stress tensor, which calls for the eigenvalue and confidence-cone machinery of spherical statistics (Fisher et al., 1987) — the underlying field values (Ts, Td, Sf) available per-fracture in the Table tab and per-orientation on the continuous grid used here can be exported and analysed externally (Section 9.3).

7.1 Theoretical Maximum Normalisation

The normalised slip-tendency field divides every grid value of τ/σn by a closed-form constant rather than by the largest value that happens to appear on the 64×128 grid:

Ts,max = (σ1 − σ3) / ( 2·√(σ1·σ3) ) the tangent from the origin to the outer σ1–σ3 Mohr circle

This is the exact theoretical maximum of τ/σn over every possible plane orientation for a diagonal principal-stress tensor, and it depends on σ1 and σ3 alone — not on σ2, not on the principal-axis orientation, and not on the discretisation of the grid. It is the same constant the calculator module's _theoretical_max_slip_tendency() uses for the per-fracture SlipTendencyNorm column, so the stereonet field and the Table/Map/Mohr colouring are numerically consistent by construction rather than only approximately. Taking the grid maximum instead would make the normalisation depend on the mesh: the grid stops short of the poles (colatitude runs 0.01 to π/2 − 0.01 rad) and samples azimuth in 128 steps, so it need not contain the optimally oriented plane.

One residual difference between field and column. Both use the formula above, but they guard the radicand differently where σ1·σ3 ≤ 0 — i.e. in tensile or effective-stress states whose Mohr circle reaches the origin. The stereonet field clamps it at an absolute 10⁻¹², whereas _theoretical_max_slip_tendency() clamps it proportionately to the stress magnitude, at 10⁻⁶·max(σ1², 1). For ordinary compressive states with σ3 > 0 the two agree exactly; only in the degenerate σ3 ≤ 0 case can the field and the per-fracture column be scaled differently, and in that regime neither normalised value should be read quantitatively.

8. Visualization Components

ComponentMatplotlib PrimitivePurpose
Primitive circleax.plot on a 361-point unit circleDefines the projection boundary (equator of the lower hemisphere).
Grid rings / degree ticksax.plot radial tick segments + ax.textOptional 30° azimuth reference marks around the perimeter, shown only when "Show Selected Plane" is enabled.
N-S / E-W crosshairax.plot two diametersQuick visual reference for cardinal directions.
Tendency fieldax.tricontourf over a Delaunay triangulationContinuous colour-coded stress-response surface across the whole hemisphere.
Fracture polesax.scatter (pickable)Discrete data points; colour and/or shape configurable.
Great-circle plane tracesax.plot (per-plane parametric sweep)Optional visualisation of full plane orientation, not just its pole.
Principal-stress markersax.scatter with square/diamond/triangle markersσ1 (square), σ2 (diamond), σ3 (triangle) axis positions.
Selected-pole haloTwo concentric ax.scatter rings + plus-markerDraws attention to the currently selected fracture.
Symbols indexCustom FancyBboxPatch + text/markersDraggable legend explaining every marker on the plot (Section 4.3).
Colour barfig.colorbar bound to a ScalarMappableQuantitative scale for the active tendency metric.
Stereonet with Show All Planes enabled
Figure 8.1 — Example rendering showing translucent great-circle traces for every loaded fracture plane overlaid with their poles, coloured by fracture susceptibility.

9. Advanced Features

9.1 General 3-D Stress Orientation Mode

When the sidebar's Orientation Mode tab is set to General 3D, the Stereonet Tab automatically re-derives the principal-stress axis positions using principal_stress_axes_fracstat() rather than the simplified Andersonian formulas, so the three principal-stress markers correctly reflect fully general (non-Andersonian) stress-field orientations such as oblique-slip regimes.

9.2 Draggable Symbols Index

Described in Section 4.3. This is a workspace-wide "Move Elements" feature: when enabled from the header, every draggable overlay across all Dynamic Analyzer tabs (Mohr's Symbols index, the Rose tab's legend, and this Stereonet index) becomes repositionable by click-and-drag, letting the analyst tailor figure layout before exporting an image for a report.

9.3 Cross-Tab Data Sharing and Export

Because the Stereonet Tab reads directly from self.results, any change made elsewhere in the workspace — loading a different data source, editing the stress tensor, applying a dip override, or switching units — is reflected here on the very next redraw with no separate synchronisation step. The Save Image toolbar button exports the current stereonet figure (including whatever position the Symbols index has been dragged to) as a high-resolution PNG through the shared save_plot_image() routine, using the export DPI configured in Settings and a filename that encodes the project name, plot name, active metric, and a timestamp.

9.4 Focal-Mechanism Aware Pole Placement

For Excel/CSV files following the Global CMT-style focal-mechanism format (Lat, Lon, FP1/FP2 Strike/Dip, optional Slip/Rake), each nodal plane becomes its own row in self.results and is plotted here exactly like any other fracture plane, letting seismological nodal-plane populations be compared on the same equal-area net and against the same resolved-stress colour field as outcrop- or map-derived fracture data.

10. Performance Optimization

10.1 Analytic Grid Sizing

The continuous tendency field is evaluated on a fixed 64×128 grid (8,192 nodes) rather than scaling with the number of loaded fractures. This keeps the triangulation and contour-fill cost constant and fast even for datasets containing many thousands of segments, since the expensive part of the redraw is the field, not the (fully vectorised) pole scatter.

10.2 Vectorised Pole Computation

Strike, dip, pole trend, and pole radius are all computed with NumPy array operations across the entire filtered DataFrame in a single pass rather than a per-row Python loop, so plotting thousands of poles remains a near-constant-time operation relative to redrawing the field.

10.3 Cheap Redraw Path for Dragging

As noted in Section 4.3, dragging the Symbols index calls _redraw_stereo_symbol_index(), which removes and rebuilds only the index's own artists (cached from the most recent full draw's arguments in self._stereo_index_last_args) rather than invoking the full _draw_stereonet() pipeline. This avoids recomputing the 8,192-node field and re-triangulating on every mouse-move event during a drag, which would otherwise make the interaction feel sluggish.

10.4 Idle-Draw Scheduling

All redraws finish with self.stereo_canvas.draw_idle() rather than a blocking draw(), deferring the actual GPU/CPU rendering work to the next idle slot in Qt's event loop so rapid successive setting changes (e.g. typing in a numeric field) do not each force an immediate, separate render pass.

11. Practical Examples

11.1 Example — Checking Orientation-Stress Compatibility

  1. Load an SVG trace map or Excel orientation table, then enter or confirm the principal stress magnitudes and orientation on the sidebar.
  2. Open the Stereonet Tab and leave Slip tendency selected as the colour metric.
  3. Observe whether the plotted poles cluster inside the "hot" (high-Ts) region of the field, near the perimeter close to the σ1 marker's perpendicular great circle, or in the "cool" low-Ts region near σ3.
  4. If most poles fall in a cool region, the current stress field is unlikely to be reactivating that fracture set, prompting a re-examination of either the assumed stress orientation or the possibility that the fractures are inherited from an earlier tectonic event.

11.2 Example — Verifying a Dip Override

  1. Load a dip-override CSV containing ID and DipDeg columns for a subset of segments.
  2. Compare the Stereonet Tab before and after loading: overridden poles will move radially (their trend is unchanged; only their distance from the centre, governed by dip, changes).
  3. Use the Show in Map button after selecting an overridden pole to confirm the correct segment was affected.

11.3 Example — Comparing Fracture Sets Against a Focal Mechanism Catalogue

  1. Load a Global CMT-style focal-mechanism Excel/CSV file alongside (in a separate session) or instead of an outcrop dataset.
  2. Enable "Show All Planes" to visualise the family of nodal-plane great circles, then toggle Critical fractures as the colour metric to see which nodal planes fall into the Critical or Unstable bands under the entered regional stress tensor.

12. Geological Interpretation

The equal-area stereonet is one of the most information-dense diagrams available to a structural geologist because it simultaneously encodes plane orientation (via pole position) and, in this module's stress-aware variant, the mechanical favourability of every conceivable orientation (via the continuous colour field). Several interpretive principles follow directly from the plotted geometry:

  • Poles near the primitive circle correspond to steeply dipping planes; poles near the centre correspond to shallow, near-horizontal planes.
  • A tight cluster of poles indicates a single, well-defined fracture set (consistent formation mechanism and stress history); a girdle (poles distributed along a great circle) suggests folding or a rotational deformation history superimposed on an originally more uniform set.
  • Poles coinciding with the high-Ts (hot) region of the field, typically forming two conjugate bands roughly 20–30° from the σ1 direction depending on the friction coefficient μ, are the orientations most favourable for shear reactivation under the current stress state — classic Andersonian conjugate shear-fracture geometry (Anderson, 1951; Morris et al., 1996).
  • Poles coinciding with high dilation tendency (near σ3) are those most favourable for tensile opening or hydraulic-fracture propagation, relevant for fluid-flow and permeability assessment.
  • A Critical fractures colouring that shows most poles in the "Stable" band, despite reasonable stress magnitudes, may indicate either a genuinely low-permeability, mechanically stable fracture network, or that the assumed pore pressure Pf or friction coefficient μ is too conservative for the field setting; poles falling in the "Near Critical" band are worth particular attention, since a modest change in either parameter could shift them into "Critical" or "Unstable" (Sibson, 1985).
  • Where the loaded data carry rake information, poles whose Wallace–Bott rake misfit is small are kinematically compatible with the assumed tensor, and a population of consistently small misfits is evidence that the tensor itself is well chosen (Wallace, 1951; Bott, 1959).

Because the field is recomputed live from the sidebar stress tensor, the Stereonet Tab also functions as an exploratory sensitivity tool: adjusting σ1/σ2/σ3 magnitudes, azimuth, pore pressure, or friction and watching how the hot/cool regions shift relative to the fixed pole population is a fast, visual way to bound plausible in-situ stress states consistent with an observed fracture set — a technique closely related to the stress-inversion methods used in borehole image-log interpretation (Barton et al., 1995; Zoback, 2007).

13. Implementation Notes

13.1 Class Structure

StereonetTab is implemented as a Python mixin class, combined with MapTab, RoseTab, MohrTab, TableTab, and MethodTab via multiple inheritance into the top-level SlipTendencyApp class (see dynamic_app.py). This means every method described in this document (e.g. _draw_stereonet, on_stereo_press) executes with self bound to the full application instance, giving it direct access to self.results, self.current_stress, self.selected_segment_id, and every other tab's shared state without any explicit message-passing layer.

13.2 Variable Naming and Persistence

All CTk variables use the stereo_ prefix (e.g. stereo_pole_color_var) to avoid collisions with the identically-purposed variables in the Mohr and Map tabs. _stereo_init_vars() uses a hasattr guard on every variable so it can be called safely multiple times (e.g. on every rebuild after a Reset Defaults) without clobbering a value the user has since changed through some other code path.

13.3 Colour-Scale Sharing

The colormap dropdown at the top of the tab is bound to the same mohr_colormap_var used by the Mohr tab (via change_mohr_colour_scale()), and the available colour-scale catalogue (Turbo, Viridis, Plasma, a custom Parula approximation, etc.) is defined once in the top-level SlipTendencyApp (_colour_scale_definitions()) and reused across every tab that offers a colour-scale choice.

13.4 Marker Shape Mapping

The Pole Shape dropdown's human-readable labels are translated to Matplotlib marker codes through a fixed lookup: Circle→o, Diamond→D, Square→s, Triangle Up→^, Star→*, Cross→X. Note this Cross mapping ('X', a filled X) differs slightly from the Mohr tab's own pole-shape dictionary, which maps Cross to 'P' (a plus/filled-cross marker) — a cosmetic difference between the two tabs' otherwise parallel styling systems.

14. Limitations

LimitationDescription
Spatially uniform stress assumptionThe continuous tendency field assumes the stress tensor is spatially uniform; it does not account for local stress perturbations near existing fractures, free surfaces, or heterogeneous lithology.
Visual clutter for dense datasetsGreat-circle plane traces ("Show All Planes") can become visually cluttered for datasets with many hundreds of fractures; the fixed low alpha (0.25) mitigates but does not eliminate this.
Non-linear angular compressionThe equal-area projection, while area-preserving, still compresses angular relationships non-linearly toward the primitive circle; visual judgements of "cluster tightness" should be corroborated with the quantitative statistics available on the Rose tab.
Dip clampingDip values are clipped to 0–90°; any input data implying an overturned or otherwise out-of-range dip will be silently clamped rather than rejected.
No rake-misfit colour metricKinematic (Wallace–Bott) rake-misfit diagnostics for focal-mechanism data appear only in the pole-inspector text panel (τ along slip, predicted rake, misfit); they are not exposed as a Stereonet colour-by metric, and the Table tab's 16 columns do not include them either.
No contoured pole densityPoles are plotted as discrete symbols only. No Kamb or Schmidt density contouring of the pole population is available (Vollmer, 1995), so judgements of cluster strength rest on visual inspection of symbol overlap.
No orientation statistics or confidence regionsEigenvalue fabric parameters, mean-vector estimates, and confidence cones about a cluster mean are not computed (Fisher et al., 1987); the tab reports only a pole count (Section 7).
No Terzaghi sampling correctionPoles are plotted one per digitised fracture, with no weighting for the orientation bias inherent in sampling planes from a line or a single outcrop face, so an apparent cluster may partly reflect the sampling geometry.
Andersonian mode covers one regimeThe simplified mode builds only the strike-slip Andersonian configuration (Sv = σ2) by default (Anderson, 1951); normal and thrust regimes require passing a different vertical axis, and the Uncertainty Analyzer defaults to the opposite convention (Section 3.5).
Stress tensor is assumed, not invertedThe field is computed from a user-entered tensor. The tab performs no formal stress inversion from the fracture or focal-mechanism population and reports no misfit statistic over the whole data set, so exploring stress states remains a manual, visual exercise (Zoback, 2007).

15. References

Sources are listed alphabetically within each group. In-text citations throughout this page link to the corresponding entry below.

15.1 Projection and Orientation Analysis

15.2 Stress, Faulting and Reactivation

15.3 Fault Kinematics and Focal Mechanisms

  • Aki, K., & Richards, P. G. (1980). Quantitative Seismology: Theory and Methods (Vol. 1), §4.2. W. H. Freeman, San Francisco.
  • Bott, M. H. P. (1959). The mechanics of oblique slip faulting. Geological Magazine, 96(2), 109–117. https://doi.org/10.1017/S0016756800059987
  • Wallace, R. E. (1951). Geometry of shearing stress and relation to faulting. The Journal of Geology, 59(2), 118–130. https://doi.org/10.1086/625831

15.4 Software

Methods statement for the Stereonet Tab. If a figure produced by this tab appears in a publication, the details below are what a reader needs in order to reproduce it, and are what the citations above assume:

Lower-hemisphere equal-area (Schmidt) stereonet of fracture poles, r = √2·sin(dip/2) (Ragan, 2009), for n planes, overlain on a continuous field of the selected stress-tendency metric evaluated on a 64×128 colatitude–trend mesh for σ₁/σ₂/σ₃ = … MPa, P_f = … MPa, C₀ = … MPa and μ = … (Jaeger et al., 2007). Slip tendency normalised by the closed-form theoretical maximum (Morris et al., 1996); principal axes built in Andersonian or general 3-D mode as stated (Anderson, 1951); rake misfits, where reported, follow the Wallace–Bott criterion (Wallace, 1951; Bott, 1959) in the Aki and Richards (1980) rake convention. Implemented in NumPy (Harris et al., 2020) and rendered with Matplotlib (Hunter, 2007). Report the full stress tensor, the orientation mode, the colour metric, and n; note that no pole-density contouring or orientation statistics are computed here (Section 14).