Well Event: Add opt-in column-aligned schedule output

Add an align_columns option to schedule generation. When enabled, each
keyword is serialized with a '--'-prefixed column-header comment and data
rows right-aligned into fixed-width columns, instead of the compact default
form. The default output is unchanged.

A new RimKeywordFactory::deckKeywordToAlignedString introspects the OPM deck
model and renders each item in its own column (consecutive defaults are not
collapsed into 'N*'). The flag is threaded through RicScheduleDataGenerator
and exposed on the GenerateSchedule scriptable method as align_columns in the
Python generate_schedule_text wrapper.
This commit is contained in:
Kristian Bendiksen committed 2026-06-01 15:45:03 +02:00
1 parent bce29354ea
commit 19c547cc2e
9 files changed
+317 -13

No files matched your search

@@ -237,6 +237,13 @@ def main():
eclipse_case=case, export_msw_for_wells=[well_path]
)
# Generate the same schedule with align_columns=True, which adds a "--"-prefixed
# column-header comment per keyword and right-aligns the data into fixed-width
# columns. Only the formatting differs from the unaligned text above.
schedule_text_aligned = timeline.generate_schedule_text(
eclipse_case=case, export_msw_for_wells=[well_path], align_columns=True
)
if schedule_text:
print(f"\n Generated schedule text ({len(schedule_text)} characters)")
print(" " + "=" * 60)
@@ -308,15 +315,20 @@ def main():
print(f" - GRUPTREE entries: {schedule_text.count('GRUPTREE')}")
print(f" - TUNING entries: {schedule_text.count('TUNING')}")
# Save to file
output_file = "generated_schedule.sch"
with open(output_file, "w") as f:
# Save both formats to file
unaligned_file = "generate_schedule_unaligned.sch"
with open(unaligned_file, "w") as f:
f.write(schedule_text)
print(f"\n Schedule text saved to: {output_file}")
print(f"\n Unaligned schedule text saved to: {unaligned_file}")
aligned_file = "generate_schedule_aligned.sch"
with open(aligned_file, "w") as f:
f.write(schedule_text_aligned)
print(f" Aligned schedule text saved to: {aligned_file}")
# Show example of generated keywords
print("\n8. Example of generated Eclipse keywords:")
print(" (See generated_schedule.sch for complete output)")
print(f" (See {unaligned_file} / {aligned_file} for complete output)")
if "WELSEGS" in schedule_text:
print("\n Sample WELSEGS segment:")
for line in lines:
@@ -1262,6 +1262,94 @@ class TestScheduleGeneration:
assert "COMPDAT" in schedule_text
def test_align_columns_adds_headers_and_alignment(self, project_with_case_and_well):
"""align_columns=True must add a '--'-prefixed column-header comment per keyword and
indent right-aligned data rows, while the default (align_columns=False) keeps the
compact form. Only the formatting should differ between the two."""
project, case, timeline = project_with_case_and_well
well_path = project.well_paths()[0]
# A perforation (COMPDAT) plus a WCONHIST keyword event gives several tabular keywords
# plus the always-present DATES keyword to exercise the aligned formatter.
timeline.add_perf_event(
event_date="2024-01-01",
well_path=well_path,
start_md=2000.0,
end_md=2200.0,
diameter=0.1,
state="OPEN",
)
timeline.add_well_keyword_event(
event_date="2024-01-01",
well_path=well_path,
keyword_name="WCONHIST",
keyword_data={
"WELL": well_path.name,
"STATUS": "OPEN",
"CMODE": "RESV",
"ORAT": 3999.99,
"VFP_TABLE": 1,
},
)
timeline.set_timestamp(timestamp="2024-01-01")
# Force the first date to a DATES keyword (instead of the default leading comment) so the
# aligned DATES column header is exercised.
aligned = timeline.generate_schedule_text(
eclipse_case=case,
export_msw_for_wells=project.well_paths(),
first_date_as_comment=False,
align_columns=True,
)
default = timeline.generate_schedule_text(
eclipse_case=case,
export_msw_for_wells=project.well_paths(),
first_date_as_comment=False,
align_columns=False,
)
print(f"\nAligned schedule text:\n{aligned}")
print(f"\nDefault schedule text:\n{default}")
# The DATES keyword is always present; aligned output prefixes its item names as a comment.
assert "--DAY" in aligned, "Aligned output should carry a DATES column header"
assert "--DAY" not in default, "Default output should not carry column headers"
# WCONHIST item names appear only as a header comment in the aligned form (their values,
# e.g. 'OPEN'/'RESV', are what show up in both forms).
assert "CMODE" in aligned and "STATUS" in aligned, (
"Aligned output should list WCONHIST item names in a header comment"
)
assert "CMODE" not in default and "STATUS" not in default, (
"Default output should not list item names"
)
# Each aligned column-header line starts with '--' and its data rows are indented two
# spaces and terminate with ' /'.
wconhist_block = aligned.split("WCONHIST\n", 1)[1]
header_line = wconhist_block.splitlines()[0]
data_line = wconhist_block.splitlines()[1]
# Header is a comment whose names are right-aligned into their columns, so it starts with
# '--' and lists WELL/STATUS/CMODE (the first column may be padded ahead of 'WELL').
assert header_line.startswith("--") and "WELL" in header_line, (
f"WCONHIST header should be a '--' comment listing item names: {header_line!r}"
)
assert data_line.startswith(" "), (
f"WCONHIST data row should be indented two spaces: {data_line!r}"
)
assert data_line.rstrip().endswith("/"), (
f"WCONHIST data row should end with '/': {data_line!r}"
)
# Per-column defaults stay as individual '1*' markers (never accumulated into 'N*').
assert "1*" in data_line and " 2*" not in data_line, (
f"WCONHIST data row should keep per-column '1*' markers: {data_line!r}"
)
# Same keywords are produced either way.
for keyword in ("DATES", "COMPDAT", "WCONHIST"):
assert keyword in aligned and keyword in default
def test_perf_completion_number_triggers_complump(self, project_with_case_and_well):
"""#13273 follow-up: a completion_number on add_perf_event must surface as a
COMPLUMP keyword (with that number) in the generated schedule.
+5
View File
@@ -289,6 +289,7 @@ def generate_schedule_text(
eclipse_case: EclipseCase,
export_msw_for_wells: List[WellPath] = [],
first_date_as_comment: bool = True,
align_columns: bool = False,
) -> str:
"""Generate Eclipse schedule text for all wells in the collection.
@@ -310,6 +311,9 @@ def generate_schedule_text(
2024") instead of a DATES keyword. This avoids a DATES entry equal
to the simulation start date, which some commercial simulators
reject. Later dates are always emitted as DATES keywords.
align_columns (bool): When True, emit each keyword with a "--"-prefixed
column-header comment and right-aligned, fixed-width columns instead
of the compact default form. Defaults to False.
Returns:
str: Eclipse schedule text containing DATES, COMPDAT, WELSEGS, WCONPROD, etc.
@@ -353,6 +357,7 @@ def generate_schedule_text(
eclipse_case=eclipse_case,
export_msw_for_wells=export_msw_for_wells,
first_date_as_comment=first_date_as_comment,
align_columns=align_columns,
)
if container and container.values:
return "".join(container.values)