It stops at the profile. Free spans and burial intervals are reported as geometric
candidates for a qualified assessment - it does not perform modal, VIV, fatigue
or stability analysis.
Sign convention before anything else. Survey depths appear both as positive
metres below sea level and as negative elevations. The convention is inferred from
the sign of every depth in the file and normalised to positive-down; mixed signs
are refused rather than guessed, because a silent sign error inverts every slope
in the profile.
Validity, then order, then resolution. Non-finite values and the common survey
null markers (-999, -999.25, -9999, 9999) are removed first. Rows are then
sorted on KP and non-increasing stations dropped, so the profile is strictly
monotonic. Only then is the resolution filter applied, keeping the first station
of every minimum_kp_spacing_m window plus both endpoints - decimating before
sorting would delete real stations and keep duplicates.
Erroneous points by robust residual. The depth series is compared against a
rolling median, and a point is flagged when its residual exceeds
outlier_threshold times a robust sigma derived from the median absolute residual
(1.4826 x MAD). A median reference and a MAD scale are used rather than a mean
and a standard deviation because a cluster of bad points would inflate both and
hide itself. Points within half a smoothing window of each end sit on a truncated
window, so their residual carries the local trend rather than an error; they are
excluded from assessment instead of being flagged. Flagged points are reported,
never silently deleted - a run of flags in one area is usually a survey-quality
signal, not noise - but they are excluded from the span and cover geometry,
because a depth that has been called wrong must not then be used to measure a gap.
Coordinates. Latitude/longitude columns are checked for swapped order, which
is detectable because a latitude cannot exceed 90 degrees, then projected onto a
local equirectangular frame about the first point. That is adequate for deriving
KP and screening geometry over a flowline and explicitly not adequate for
positioning work.
Spans and cover from one signed quantity. With both depths present,
cover = depth_to_top - seabed_depth in the positive-down frame. Positive cover
means the pipe top sits below the seabed and the station belongs to a burial
interval; negative cover is a gap, and consecutive stations whose gap exceeds
span_gap_threshold_m are grouped into a span candidate with its length, maximum
and mean gap. Grouping consecutive stations, rather than counting points, is what
makes the output comparable to a span list.
Repeat surveys on a common grid. Two processed surveys rarely share stations,
so both are linearly interpolated onto a common KP grid over their overlap and
differenced. Positive change means the pipe top sits deeper in the repeat survey.
Intervals exceeding change_threshold_m are grouped and labelled lowering or
lifting; the skill deliberately does not call this movement, because datum and
tide differences produce the same signature.
from pipeline_survey_processing import PipelineSurveyProcessor
processor = PipelineSurveyProcessor(
minimum_kp_spacing_m=5.0,
outlier_threshold=4.0,
minimum_residual_m=0.1,
smoothing_window=11,
span_gap_threshold_m=0.05,
)
records = [
{"kp_m": 0.0, "depth_to_top_m": -298.4, "seabed_depth_m": -298.2},
{"kp_m": 5.0, "depth_to_top_m": -298.2, "seabed_depth_m": -298.0},
{"kp_m": 10.0, "depth_to_top_m": -999.25},
{"kp_m": 15.0, "depth_to_top_m": -240.0, "seabed_depth_m": -297.7},
{"kp_m": 20.0, "depth_to_top_m": -297.5, "seabed_depth_m": -297.3},
]
result = processor.process(
records=records,
pipeline_id="1192-Y-101",
survey_id="2010-survey",
start_kp_m=0.0,
end_kp_m=20.0,
outer_diameter_m=0.3239,
)
print(result.depth_convention)
print(result.rejected_point_count)
print([point.kp_m for point in result.flagged_points])
print(result.span_candidates)
for entry in result.processing_log:
print(entry)
handoff = PipelineSurveyProcessor.to_neqsim_elevation_profile(result, section_count=20)
older = processor.process(records=records, pipeline_id="1192-Y-101", survey_id="2006-survey")
change = processor.compare(baseline=older, repeat=result, change_threshold_m=0.2)
print(change.max_lowering_m, change.max_lifting_m, change.changed_intervals)