Skip to content

Commit 326ae0d

Browse files
committed
#14063 Well Event: Sort schedule output by well name
The well list driving schedule generation originates from a std::set<RimWellPath*>, so it was ordered by pointer address and the per-well keyword records (WELSPECS, COMPDAT, COMPLUMP, MSW keywords, well control) came out in a non-deterministic order that only looked sorted by coincidence. Sort the well list once by the deck export name in generateSchedule, so every per-well keyword is emitted in a stable, well-name-sorted order, matching the convention already used by the file-based completion export. Schedule-level keywords (GRUPTREE, RPTRST, TUNING) are left untouched.
1 parent 393e52c commit 326ae0d

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

ApplicationLibCode/Commands/CompletionExportCommands/RicScheduleDataGenerator.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "opm/input/eclipse/Deck/DeckKeyword.hpp"
3636
#include "opm/input/eclipse/Deck/DeckRecord.hpp"
3737

38+
#include <algorithm>
3839
#include <map>
3940
#include <set>
4041

@@ -54,10 +55,23 @@ QString RicScheduleDataGenerator::generateSchedule( const RimWellEventTimeline&
5455
result += "-- Schedule data based on well events\n";
5556
result += "--\n";
5657

58+
// The incoming well list is ordered by pointer (it originates from a std::set), which makes the
59+
// per-well keyword records appear in a non-deterministic order. Sort once by the deck export
60+
// name so every per-well keyword (WELSPECS, COMPDAT, COMPLUMP, WELSEGS/COMPSEGS, WSEGVALV/
61+
// WSEGAICD, well control) is emitted in a stable, well-name-sorted order.
62+
auto exportName = []( const RimWellPath* well ) -> QString
63+
{ return well && well->completionSettings() ? well->completionSettings()->wellNameForExport() : QString(); };
64+
65+
std::vector<RimWellPath*> sortedWellPaths = wellPaths;
66+
std::stable_sort( sortedWellPaths.begin(),
67+
sortedWellPaths.end(),
68+
[&exportName]( const RimWellPath* a, const RimWellPath* b )
69+
{ return QString::compare( exportName( a ), exportName( b ) ) < 0; } );
70+
5771
// Generate section for each date
5872
for ( const auto& date : dates )
5973
{
60-
QString dateSection = generateDateSection( timeline, eclipseCase, wellPaths, date, mswWells );
74+
QString dateSection = generateDateSection( timeline, eclipseCase, sortedWellPaths, date, mswWells );
6175
if ( !dateSection.isEmpty() )
6276
{
6377
result += dateSection;

GrpcInterface/Python/rips/tests/test_well_events.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,57 @@ def test_keywords_grouped_across_wells(self, project_with_case_and_well):
15161516
f"Well {wp.name!r} missing from grouped WELSPECS block: {welspecs_block!r}"
15171517
)
15181518

1519+
def test_per_well_keywords_sorted_by_well_name(self, project_with_case_and_well):
1520+
"""Per-well keyword records are emitted in deck-name-sorted well order, so WELSPECS
1521+
and COMPDAT share the same ascending well order rather than an arbitrary one.
1522+
1523+
Note: the fixture imports wells A then B, so insertion order already matches name
1524+
order; this test locks in the deterministic ascending ordering and cross-keyword
1525+
consistency that the sort guarantees.
1526+
"""
1527+
project, case, timeline = project_with_case_and_well
1528+
well_paths = project.well_paths()
1529+
assert len(well_paths) >= 2, (
1530+
"Test requires at least two well paths in the fixture"
1531+
)
1532+
1533+
for wp in well_paths[:2]:
1534+
timeline.add_perf_event(
1535+
event_date="2024-01-01",
1536+
well_path=wp,
1537+
start_md=2000.0,
1538+
end_md=2200.0,
1539+
diameter=0.1,
1540+
state="OPEN",
1541+
)
1542+
1543+
timeline.set_timestamp(timestamp="2024-12-31")
1544+
schedule_text = timeline.generate_schedule_text(
1545+
eclipse_case=case, export_msw_for_wells=project.well_paths()
1546+
)
1547+
print(f"\nSchedule text for sorted well order:\n{schedule_text}")
1548+
1549+
export_names = sorted(wp.name.replace(" ", "") for wp in well_paths[:2])
1550+
1551+
def first_positions(block):
1552+
squashed = block.replace(" ", "")
1553+
return [squashed.find(name) for name in export_names]
1554+
1555+
welspecs_block = schedule_text.split("WELSPECS\n", 1)[1].split("\n/\n", 1)[0]
1556+
compdat_block = schedule_text.split("COMPDAT\n", 1)[1].split("\n/\n", 1)[0]
1557+
1558+
for block_name, block in (
1559+
("WELSPECS", welspecs_block),
1560+
("COMPDAT", compdat_block),
1561+
):
1562+
positions = first_positions(block)
1563+
assert all(p >= 0 for p in positions), (
1564+
f"Both wells must appear in the {block_name} block: {block!r}"
1565+
)
1566+
assert positions == sorted(positions), (
1567+
f"{block_name} wells not in ascending name order {export_names}: {block!r}"
1568+
)
1569+
15191570
def test_welsegs_compsegs_not_merged_across_wells(self, project_with_case_and_well):
15201571
"""WELSEGS and COMPSEGS carry a per-well header record and therefore cannot
15211572
be merged across wells: each MSW well must get its own keyword block on the

0 commit comments

Comments
 (0)