22
33from __future__ import annotations
44
5+ import io
56import time
67from collections .abc import Callable
78from datetime import timedelta
89from typing import TYPE_CHECKING
910
1011import humanize
1112import numpy as np
13+ import scipy as sp
1214from iterative_ensemble_smoother import LocalizedESMDA
1315
14- from ert .analysis .event import AnalysisEvent , AnalysisStatusEvent
16+ from ert .analysis .event import (
17+ AnalysisEvent ,
18+ AnalysisRhoMatrixEvent ,
19+ AnalysisStatusEvent ,
20+ )
1521from ert .config import Field , SurfaceConfig
1622from ert .field_utils import (
1723 AxisOrientation ,
2733 import numpy .typing as npt
2834
2935 from ert .config import ParameterConfig
36+ from ert .storage .local_experiment import LocalExperiment
3037
3138 from ._protocol import ObservationContext
3239
@@ -39,10 +46,12 @@ def __init__(
3946 enkf_truncation : float ,
4047 param_type : type [Field | SurfaceConfig ],
4148 progress_callback : Callable [[AnalysisEvent ], None ],
49+ experiment : LocalExperiment | None = None ,
4250 ) -> None :
4351 self ._enkf_truncation = enkf_truncation
4452 self ._param_type = param_type
4553 self ._progress_callback = progress_callback
54+ self ._experiment = experiment
4655 self ._obs_loc : ObservationLocations | None = None
4756 self ._smoother : LocalizedESMDA | None = None
4857 self ._ensemble_size : int = 0
@@ -125,6 +134,19 @@ def update(
125134
126135 return result
127136
137+ def _load_rho_from_storage (
138+ self , param_name : str
139+ ) -> npt .NDArray [np .floating ] | None :
140+ """Try to load a cached rho matrix from experiment blob storage."""
141+ if self ._experiment is None :
142+ return None
143+ obs_keys = (
144+ self ._obs_loc .observation_keys
145+ if self ._obs_loc is not None and self ._obs_loc .observation_keys
146+ else None
147+ )
148+ return self ._experiment .load_rho_matrix (param_name , obs_keys )
149+
128150 def _full_localization_matrix (
129151 self ,
130152 num_params : int ,
@@ -170,32 +192,50 @@ def _update_field(
170192 if ertbox .axis_orientation is None :
171193 raise ValueError ("Field grid axis orientation must be defined" )
172194
173- xpos , ypos = transform_positions_to_local_field_coordinates (
174- ertbox .origin ,
175- ertbox .rotation_angle ,
176- self ._obs_loc .xpos ,
177- self ._obs_loc .ypos ,
178- )
195+ cached_rho = self ._load_rho_from_storage (param_config .name )
196+ if cached_rho is not None :
197+ rho_2d = cached_rho
198+ else :
199+ xpos , ypos = transform_positions_to_local_field_coordinates (
200+ ertbox .origin ,
201+ ertbox .rotation_angle ,
202+ self ._obs_loc .xpos ,
203+ self ._obs_loc .ypos ,
204+ )
179205
180- ellipse_rotation = transform_local_ellipse_angle_to_local_coords (
181- ertbox .rotation_angle ,
182- np .zeros_like (self ._obs_loc .main_range ),
183- )
206+ ellipse_rotation = transform_local_ellipse_angle_to_local_coords (
207+ ertbox .rotation_angle ,
208+ np .zeros_like (self ._obs_loc .main_range ),
209+ )
184210
185- rho_matrix = calc_rho_for_2d_grid_layer (
186- nx = ertbox .nx ,
187- ny = ertbox .ny ,
188- xinc = ertbox .xinc ,
189- yinc = ertbox .yinc ,
190- obs_xpos = xpos ,
191- obs_ypos = ypos ,
192- obs_main_range = self ._obs_loc .main_range ,
193- obs_perp_range = self ._obs_loc .main_range ,
194- obs_anisotropy_angle = ellipse_rotation ,
195- axis_orientation = ertbox .axis_orientation ,
196- )
211+ rho_matrix = calc_rho_for_2d_grid_layer (
212+ nx = ertbox .nx ,
213+ ny = ertbox .ny ,
214+ xinc = ertbox .xinc ,
215+ yinc = ertbox .yinc ,
216+ obs_xpos = xpos ,
217+ obs_ypos = ypos ,
218+ obs_main_range = self ._obs_loc .main_range ,
219+ obs_perp_range = self ._obs_loc .main_range ,
220+ obs_anisotropy_angle = ellipse_rotation ,
221+ axis_orientation = ertbox .axis_orientation ,
222+ )
197223
198- rho_2d = rho_matrix .reshape (ertbox .nx * ertbox .ny , - 1 )
224+ rho_2d = rho_matrix .reshape (ertbox .nx * ertbox .ny , - 1 )
225+
226+ if self ._obs_loc .observation_keys :
227+ rho_sparse = sp .sparse .csc_matrix (rho_2d )
228+ buf = io .BytesIO ()
229+ sp .sparse .save_npz (buf , rho_sparse )
230+ self ._progress_callback (
231+ AnalysisRhoMatrixEvent (
232+ param_name = param_config .name ,
233+ observation_keys = self ._obs_loc .observation_keys ,
234+ shape = rho_2d .shape ,
235+ data_type = str (rho_2d .dtype ),
236+ matrix_bytes = buf .getvalue (),
237+ )
238+ )
199239
200240 for param_batch_idx in batches :
201241 update_idx = param_batch_idx [non_zero_variance_mask [param_batch_idx ]]
@@ -242,37 +282,56 @@ def _update_surface(
242282
243283 assert self ._obs_loc is not None
244284
245- xpos , ypos = transform_positions_to_local_field_coordinates (
246- (param_config .xori , param_config .yori ),
247- param_config .rotation ,
248- self ._obs_loc .xpos ,
249- self ._obs_loc .ypos ,
250- )
285+ cached_rho = self ._load_rho_from_storage (param_config .name )
286+ if cached_rho is not None :
287+ rho_flat = cached_rho
288+ else :
289+ xpos , ypos = transform_positions_to_local_field_coordinates (
290+ (param_config .xori , param_config .yori ),
291+ param_config .rotation ,
292+ self ._obs_loc .xpos ,
293+ self ._obs_loc .ypos ,
294+ )
251295
252- rotation_angle = transform_local_ellipse_angle_to_local_coords (
253- param_config .rotation ,
254- np .zeros_like (self ._obs_loc .main_range , dtype = np .float64 ),
255- )
296+ rotation_angle = transform_local_ellipse_angle_to_local_coords (
297+ param_config .rotation ,
298+ np .zeros_like (self ._obs_loc .main_range , dtype = np .float64 ),
299+ )
256300
257- if param_config .yflip != 1 :
258- raise ValueError (
259- f"Expected SurfaceConfig.yflip == 1, got { param_config .yflip } "
301+ if param_config .yflip != 1 :
302+ raise ValueError (
303+ f"Expected SurfaceConfig.yflip == 1, got { param_config .yflip } "
304+ )
305+
306+ rho_matrix = calc_rho_for_2d_grid_layer (
307+ nx = param_config .ncol ,
308+ ny = param_config .nrow ,
309+ xinc = param_config .xinc ,
310+ yinc = param_config .yinc ,
311+ obs_xpos = xpos ,
312+ obs_ypos = ypos ,
313+ obs_main_range = self ._obs_loc .main_range ,
314+ obs_perp_range = self ._obs_loc .main_range ,
315+ obs_anisotropy_angle = rotation_angle ,
316+ axis_orientation = AxisOrientation .LEFT_HANDED ,
260317 )
261318
262- rho_matrix = calc_rho_for_2d_grid_layer (
263- nx = param_config .ncol ,
264- ny = param_config .nrow ,
265- xinc = param_config .xinc ,
266- yinc = param_config .yinc ,
267- obs_xpos = xpos ,
268- obs_ypos = ypos ,
269- obs_main_range = self ._obs_loc .main_range ,
270- obs_perp_range = self ._obs_loc .main_range ,
271- obs_anisotropy_angle = rotation_angle ,
272- axis_orientation = AxisOrientation .LEFT_HANDED ,
273- )
319+ rho_flat = rho_matrix .reshape (- 1 , rho_matrix .shape [- 1 ])
320+
321+ if self ._obs_loc .observation_keys :
322+ rho_sparse = sp .sparse .csc_matrix (rho_flat )
323+ buf = io .BytesIO ()
324+ sp .sparse .save_npz (buf , rho_sparse )
325+ self ._progress_callback (
326+ AnalysisRhoMatrixEvent (
327+ param_name = param_config .name ,
328+ observation_keys = self ._obs_loc .observation_keys ,
329+ shape = rho_flat .shape ,
330+ data_type = str (rho_flat .dtype ),
331+ matrix_bytes = buf .getvalue (),
332+ )
333+ )
274334
275- rho_flat = rho_matrix .reshape (- 1 , rho_matrix .shape [- 1 ])
276335 for param_batch_idx in batches :
277336 update_idx = param_batch_idx [non_zero_variance_mask [param_batch_idx ]]
278337 if update_idx .size == 0 :
0 commit comments