import re
from apsg.config import apsg_conf
from apsg.helpers._math import asind, atan2d, cosd, sind
NOTATIONS = ("dd", "rhr", "quadrant")
# NOTATION TRANSORMATIONS
def fol2vec_dd(azi, inc):
"""Convert dip direction/dip to normal vector components."""
return -cosd(azi) * sind(inc), -sind(azi) * sind(inc), cosd(inc)
def fol2vec_rhr(strike, dip):
"""Convert strike/dip (RHR) to normal vector components."""
return fol2vec_dd(strike + 90, dip)
def _notation_fn(table):
"""Look up the conversion function for the active `apsg_conf.notation`."""
try:
return table[apsg_conf.notation]
except KeyError:
raise ValueError(
f"Unknown notation {apsg_conf.notation!r}, expected one of {NOTATIONS}"
) from None
[docs]
def geo2vec_planar(*args):
"""Transform geological measurement of plane to normal vector.
Conversion is done according to `notation` configuration. Quadrant notation
is a textual convention (see `parse_quadrant_planar`) and is not accepted here.
Args:
azi (float): dip direction or strike
inc (float): dip
"""
return _notation_fn(
{"dd": fol2vec_dd, "rhr": fol2vec_rhr, "quadrant": fol2vec_rhr}
)(*args)
##############################
def lin2vec_dd(azi, inc):
"""Convert plunge direction/plunge to vector components."""
return cosd(azi) * cosd(inc), sind(azi) * cosd(inc), sind(inc)
[docs]
def geo2vec_linear(*args):
"""Transform geological measurement of line to vector.
Args:
azi (float): plunge direction
inc (float): plunge
"""
return lin2vec_dd(*args)
##############################
def vec2fol_dd(v):
"""Convert normal vector to dip direction/dip."""
n = v.uv()
if n.z < 0:
n = -n
return (atan2d(n.y, n.x) + 180) % 360, 90 - asind(n.z)
def vec2fol_dd_signed(v):
"""Convert normal vector to signed dip direction/dip."""
n = v.uv()
return (atan2d(n.y, n.x) + 180) % 360, 90 - asind(n.z)
def vec2fol_rhr(v):
"""Convert normal vector to strike/dip (RHR)."""
n = v.uv()
if n.z < 0:
n = -n
return (atan2d(n.y, n.x) + 90) % 360, 90 - asind(n.z)
def vec2fol_rhr_signed(v):
"""Convert normal vector to signed strike/dip (RHR)."""
n = v.uv()
return (atan2d(n.y, n.x) + 90) % 360, 90 - asind(n.z)
def vec2geo_planar_signed(arg):
"""Transform normal vector to signed planar measurement."""
return _notation_fn(
{
"dd": vec2fol_dd_signed,
"rhr": vec2fol_rhr_signed,
"quadrant": vec2fol_rhr_signed,
}
)(arg)
[docs]
def vec2geo_planar(arg):
"""Transform normal vector to geological measurement of plane.
Conversion is done according to `notation` configuration. Under `"quadrant"`
notation this still returns numeric RHR strike/dip - use `format_planar` to
render it as quadrant text.
Args:
v (Vector3): ``Vector3`` like object
"""
return _notation_fn(
{"dd": vec2fol_dd, "rhr": vec2fol_rhr, "quadrant": vec2fol_rhr}
)(arg)
##############################
def vec2lin_dd(v):
"""Convert vector to plunge direction/plunge."""
n = v.uv()
if n.z < 0:
n = -n
return atan2d(n.y, n.x) % 360, asind(n.z)
def vec2lin_dd_signed(v):
"""Convert vector to signed plunge direction/plunge."""
n = v.uv()
return atan2d(n.y, n.x) % 360, asind(n.z)
def vec2geo_linear_signed(arg):
"""Transform vector to signed linear measurement."""
return _notation_fn(
{
"dd": vec2lin_dd_signed,
"rhr": vec2lin_dd_signed,
"quadrant": vec2lin_dd_signed,
}
)(arg)
[docs]
def vec2geo_linear(arg):
"""Transform vector to geological measurement of line.
Args:
v (Vector3): ``Vector3`` like object
"""
return vec2lin_dd(arg)
##############################
# QUADRANT NOTATION (textual strike/bearing convention)
_BEARING_RE = re.compile(
r"^\s*([NS])\s*(\d{1,2}(?:\.\d+)?)\s*([EW])\s*$", re.IGNORECASE
)
_DIP_QUADRANTS = {"NE": 45, "SE": 135, "SW": 225, "NW": 315} # bucket centers
_PLANAR_QUADRANT_RE = re.compile(
r"^\s*([NSns]\s*\d{1,2}(?:\.\d+)?\s*[EWew])\s*,\s*(\d{1,2}(?:\.\d+)?)\s*(NE|SE|SW|NW)\s*$"
)
_LINEAR_QUADRANT_RE = re.compile(
r"^\s*([NSns]\s*\d{1,2}(?:\.\d+)?\s*[EWew])\s*,\s*(\d{1,2}(?:\.\d+)?)\s*$"
)
def azi2bearing(azi):
"""Format azimuth (0-360 degrees) as quadrant bearing string, e.g. 45 -> 'N45E'."""
azi = azi % 360
if azi <= 90:
return f"N{azi:.0f}E"
if azi <= 180:
return f"S{180 - azi:.0f}E"
if azi <= 270:
return f"S{azi - 180:.0f}W"
return f"N{360 - azi:.0f}W"
def bearing2azi(s):
"""Parse quadrant bearing string, e.g. 'N45E', to azimuth in degrees 0-360."""
m = _BEARING_RE.match(s)
if not m:
raise ValueError(f"Cannot parse quadrant bearing: {s!r}")
ns, angle, ew = m.group(1).upper(), float(m.group(2)), m.group(3).upper()
if not 0 <= angle <= 90:
raise ValueError(f"Quadrant bearing angle must be 0-90: {s!r}")
return {
("N", "E"): angle,
("N", "W"): 360 - angle,
("S", "E"): 180 - angle,
("S", "W"): 180 + angle,
}[(ns, ew)]
def _dip_qualifier(azi):
"""Bucket a dip-direction azimuth into cardinal quadrant NE/SE/SW/NW."""
return ("NE", "SE", "SW", "NW")[int(azi % 360 // 90)]
[docs]
def parse_quadrant_planar(s):
"""Parse quadrant planar measurement, e.g. 'N30E,40NW', to RHR (strike, dip)."""
m = _PLANAR_QUADRANT_RE.match(s)
if not m:
raise ValueError(f"Cannot parse quadrant planar measurement: {s!r}")
bearing, dip, dip_quadrant = m.groups()
strike, dip = bearing2azi(bearing), float(dip)
target = _DIP_QUADRANTS[dip_quadrant.upper()]
if abs((strike + 90 - target + 180) % 360 - 180) > 90:
strike = (strike + 180) % 360
return strike, dip
def format_quadrant_planar(strike, dip):
"""Format RHR (strike, dip) as quadrant planar measurement string.
A strike is an axial (bidirectional) line, so `strike` and `strike + 180`
are the same physical line; by convention it is reported as a bearing
measured from North (e.g. N30E or N80W rather than its S-quadrant
equivalent), independent of the dip qualifier, which is derived from the
actual dip azimuth `strike + 90`.
"""
dip_azi = (strike + 90) % 360
display_strike = strike if (strike <= 90 or strike > 270) else strike - 180
return f"{azi2bearing(display_strike)},{dip:.0f}{_dip_qualifier(dip_azi)}"
[docs]
def parse_quadrant_linear(s):
"""Parse quadrant linear measurement, e.g. 'N45E,30', to (trend, plunge)."""
m = _LINEAR_QUADRANT_RE.match(s)
if not m:
raise ValueError(f"Cannot parse quadrant linear measurement: {s!r}")
bearing, plunge = m.groups()
return bearing2azi(bearing), float(plunge)
def format_quadrant_linear(azi, inc):
"""Format (trend, plunge) as quadrant linear measurement string."""
return f"{azi2bearing(azi)},{inc:.0f}"