|
27 | 27 | ParameterConfig, |
28 | 28 | SummaryConfig, |
29 | 29 | ) |
| 30 | +from ert.exceptions import StorageError |
30 | 31 | from ert.substitutions import substitute_runpath_name |
31 | 32 |
|
32 | 33 | from .load_status import LoadResult |
@@ -1086,6 +1087,149 @@ def get_observations_and_responses( |
1086 | 1087 | pl.col("response_key").cast(pl.String).alias("response_key") |
1087 | 1088 | ) |
1088 | 1089 |
|
| 1090 | + def get_rft_observations_and_responses( |
| 1091 | + self, |
| 1092 | + ) -> pl.DataFrame: |
| 1093 | + """Fetches and aligns RFT observations with their corresponding |
| 1094 | + simulated responses from an ensemble. |
| 1095 | +
|
| 1096 | + Returns a DataFrame with observation/response data using |
| 1097 | + column names equal to the ones used by the subscript forward model |
| 1098 | + MERGE_RFT_ERTOBS, and compatible with the webviz-subsurface RftPlotter. |
| 1099 | + """ |
| 1100 | + rft_observations = self.experiment.observations.get("rft") |
| 1101 | + if rft_observations is None or rft_observations.is_empty(): |
| 1102 | + raise StorageError("No RFT observations found in experiment") |
| 1103 | + |
| 1104 | + if "rft" not in self.experiment.response_configuration: |
| 1105 | + raise KeyError("No RFT response configuration found in experiment") |
| 1106 | + |
| 1107 | + realizations = self.get_realization_list_with_responses() |
| 1108 | + if not realizations: |
| 1109 | + raise StorageError("No realizations with responses found") |
| 1110 | + |
| 1111 | + # Build date-to-report_step mapping from summary responses if available |
| 1112 | + date_to_report_step: dict[str, int] = {} |
| 1113 | + if "summary" in self.experiment.response_configuration: |
| 1114 | + try: |
| 1115 | + summary_df = self.load_responses("summary", (realizations[0],)) |
| 1116 | + times = summary_df["time"].unique().sort() |
| 1117 | + for report_step, time in enumerate(times): |
| 1118 | + date_str = time.strftime("%Y-%m-%d") |
| 1119 | + date_to_report_step[date_str] = report_step |
| 1120 | + except (KeyError, IndexError): |
| 1121 | + pass # No summary data available, will use default |
| 1122 | + |
| 1123 | + observations = rft_observations.with_columns( |
| 1124 | + pl.int_range(pl.len()).over("well").alias("order"), |
| 1125 | + ) |
| 1126 | + |
| 1127 | + join_keys = ["well", "date", "east", "north", "tvd"] |
| 1128 | + observed_values = {k: observations[k].unique() for k in join_keys} |
| 1129 | + |
| 1130 | + pivot_index = [ |
| 1131 | + "well", |
| 1132 | + "date", |
| 1133 | + "realization", |
| 1134 | + "response_zone", |
| 1135 | + "east", |
| 1136 | + "north", |
| 1137 | + "tvd", |
| 1138 | + "i", |
| 1139 | + "j", |
| 1140 | + "k", |
| 1141 | + ] |
| 1142 | + |
| 1143 | + output_columns = [ |
| 1144 | + "order", |
| 1145 | + "east", |
| 1146 | + "north", |
| 1147 | + "md", |
| 1148 | + "tvd", |
| 1149 | + "zone", |
| 1150 | + "pressure", |
| 1151 | + "swat", |
| 1152 | + "sgas", |
| 1153 | + "soil", |
| 1154 | + "valid_zone", |
| 1155 | + "is_active", |
| 1156 | + "i", |
| 1157 | + "j", |
| 1158 | + "k", |
| 1159 | + "well", |
| 1160 | + "date", |
| 1161 | + "realization", |
| 1162 | + "report_step", |
| 1163 | + "observations", |
| 1164 | + "std", |
| 1165 | + ] |
| 1166 | + |
| 1167 | + result_frames: list[pl.DataFrame] = [] |
| 1168 | + |
| 1169 | + for real in sorted(realizations): |
| 1170 | + responses = ( |
| 1171 | + self.load_responses("rft", (real,)) |
| 1172 | + .with_columns( |
| 1173 | + pl.col("property").str.to_lowercase(), |
| 1174 | + ) |
| 1175 | + .rename({"zone": "response_zone"}) |
| 1176 | + ) |
| 1177 | + |
| 1178 | + for col, values in observed_values.items(): |
| 1179 | + responses = responses.filter( |
| 1180 | + pl.col(col).is_in(values.implode(), nulls_equal=True) |
| 1181 | + ) |
| 1182 | + |
| 1183 | + pivoted = responses.pivot( |
| 1184 | + on="property", |
| 1185 | + index=pivot_index, |
| 1186 | + values="values", |
| 1187 | + ) |
| 1188 | + |
| 1189 | + for col in ["pressure", "sgas", "swat"]: |
| 1190 | + if col not in pivoted.columns: |
| 1191 | + pivoted = pivoted.with_columns( |
| 1192 | + pl.lit(None).cast(pl.Float32).alias(col) |
| 1193 | + ) |
| 1194 | + |
| 1195 | + pivoted = pivoted.with_columns( |
| 1196 | + pl.col("pressure").is_not_null().alias("is_active") |
| 1197 | + ) |
| 1198 | + |
| 1199 | + joined = ( |
| 1200 | + observations.join( |
| 1201 | + pivoted, |
| 1202 | + how="left", |
| 1203 | + on=join_keys, |
| 1204 | + nulls_equal=True, |
| 1205 | + ) |
| 1206 | + .with_columns( |
| 1207 | + pl.col("zone") |
| 1208 | + .eq_missing(pl.col("response_zone")) |
| 1209 | + .alias("valid_zone"), |
| 1210 | + (1 - pl.col("sgas") - pl.col("swat")).alias("soil"), |
| 1211 | + pl.col("is_active").fill_null(False), |
| 1212 | + pl.col("date") |
| 1213 | + .replace_strict(date_to_report_step, default=0) |
| 1214 | + .alias("report_step"), |
| 1215 | + ) |
| 1216 | + .select(output_columns) |
| 1217 | + ) |
| 1218 | + |
| 1219 | + result_frames.append(joined) |
| 1220 | + |
| 1221 | + return pl.concat(result_frames, how="vertical").rename( |
| 1222 | + { |
| 1223 | + "east": "utm_x", |
| 1224 | + "north": "utm_y", |
| 1225 | + "md": "measured_depth", |
| 1226 | + "tvd": "true_vertical_depth", |
| 1227 | + "date": "time", |
| 1228 | + "observations": "observed", |
| 1229 | + "std": "error", |
| 1230 | + } |
| 1231 | + ) |
| 1232 | + |
1089 | 1233 | @property |
1090 | 1234 | def everest_realization_info(self) -> dict[int, EverestRealizationInfo] | None: |
1091 | 1235 | return self._index.everest_realization_info |
|
0 commit comments