From 8138c09a5734c7e2661abddea99b8ab3e1d2ec15 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 24 Jun 2026 12:09:38 -0700 Subject: [PATCH 1/6] Add check if residuals need to be computed --- simpeg/dask/inverse_problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg/dask/inverse_problem.py b/simpeg/dask/inverse_problem.py index 5caf1faec5..4061d13b84 100644 --- a/simpeg/dask/inverse_problem.py +++ b/simpeg/dask/inverse_problem.py @@ -57,7 +57,7 @@ def get_dpred(self, m, f=None, return_residuals=False): def dask_evalFunction(self, m, return_g=True, return_H=True): """evalFunction(m, return_g=True, return_H=True)""" - if not np.allclose(self.model, m): + if not np.allclose(self.model, m) or getattr(self, "residuals", None) is None: self.model = m self.dpred, self.residuals = self.get_dpred(m, return_residuals=True) From 10ff10be6a203848b4652953311f2492aa54ee81 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 24 Jun 2026 12:10:16 -0700 Subject: [PATCH 2/6] Skip initial write if iteration >0 --- simpeg/directives/_save_geoh5.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/simpeg/directives/_save_geoh5.py b/simpeg/directives/_save_geoh5.py index 9fb234faa5..9a5c7fd32e 100644 --- a/simpeg/directives/_save_geoh5.py +++ b/simpeg/directives/_save_geoh5.py @@ -55,7 +55,8 @@ def initialize(self): if self.open_geoh5 and not getattr(self._workspace, "_geoh5", None): self._workspace.open(mode="r+") - self.write(0) + if getattr(self.opt, "iter", 0) == 0: + self.write(0) if self.close_geoh5: self._workspace.close() @@ -421,9 +422,6 @@ def write(self, iteration: int, **_): with open(filepath, "a", encoding="utf-8") as file: date_time = datetime.now().strftime("%b-%d-%Y:%H:%M:%S") - - if len(log) == 2: # First iteration with 0th iter - file.write(f"{0} " + " ".join(log[0]) + f" {date_time}\n") file.write(f"{iteration-1} " + " ".join(log[-1]) + f" {date_time}\n") self.save_log() From 5a2e0046e60076c0b499108678816e5ecf4a1966 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 24 Jun 2026 12:10:50 -0700 Subject: [PATCH 3/6] Increase number of decimal in prints. Allow opt to start at iteration > 0 --- simpeg/optimization.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/simpeg/optimization.py b/simpeg/optimization.py index 020e9a0155..5b1b2a207b 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -220,7 +220,7 @@ class IterationPrinters(object): "title": "f", "value": lambda M: M.f, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } norm_g = { "title": "|proj(x-g)-x|", @@ -228,7 +228,7 @@ class IterationPrinters(object): None if M.iter == 0 else norm(M.projection(M.xc - M.g) - M.xc) ), "width": 15, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } totalLS = { "title": "LS", @@ -247,7 +247,7 @@ class IterationPrinters(object): "title": "ft", "value": lambda M: M._LS_ft, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } LS_t = { "title": "t", @@ -259,14 +259,14 @@ class IterationPrinters(object): "title": "f + alp*g.T*p", "value": lambda M: M.f + M.LSreduction * M._LS_descent, "width": 16, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } LS_WolfeCurvature = { "title": "alp*g.T*p", "str": "%d : ft = %1.4e >= alp*descent = %1.4e", "value": lambda M: M.LScurvature * M._LS_descent, "width": 16, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } itType = { @@ -298,44 +298,44 @@ class IterationPrinters(object): "title": "beta", "value": lambda M: M.parent.beta, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } phi_d = { "title": "phi_d", "value": lambda M: M.parent.phi_d, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } phi_m = { "title": "phi_m", "value": lambda M: M.parent.phi_m, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } phi_s = { "title": "phi_s", "value": lambda M: M.parent.phi_s, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } phi_x = { "title": "phi_x", "value": lambda M: M.parent.phi_x, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } phi_y = { "title": "phi_y", "value": lambda M: M.parent.phi_y, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } phi_z = { "title": "phi_z", "value": lambda M: M.parent.phi_z, "width": 10, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } iterationCG = { @@ -349,14 +349,14 @@ class IterationPrinters(object): "title": "CG |Ax-b|/|b|", "value": lambda M: getattr(M, "cg_rel_resid", None), "width": 15, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } iteration_CG_abs_residual = { "title": "CG |Ax-b|", "value": lambda M: getattr(M, "cg_abs_resid", None), "width": 11, - "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.3e}", } @@ -397,7 +397,8 @@ class Minimize(object): def __init__(self, **kwargs): set_kwargs(self, **kwargs) - + self.iter = 0 + self.f0 = None self.stoppersLS = [ StoppingCriteria.armijoGoldstein, StoppingCriteria.iterationLS, @@ -546,8 +547,6 @@ def startup(self, x0: np.ndarray) -> None: x0 : numpy.ndarray initial x """ - - self.iter = 0 self.iterLS = 0 self.stopNextIteration = False @@ -644,7 +643,7 @@ def finish(self) -> None: pass def stoppingCriteria(self, inLS: bool = False) -> bool: - if self.iter == 0: + if self.f0 is None: self.f0 = self.f self.g0 = self.g return check_stoppers(self, self.stoppers if not inLS else self.stoppersLS) From 0c15c3a3eb4c78e6a8ad8c637347ccfc8e97c95a Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 29 Jun 2026 11:05:08 -0700 Subject: [PATCH 4/6] Refactor directives for restart --- simpeg/directives/_directives.py | 38 +++++++++++++++++++++++++++----- simpeg/directives/_save_geoh5.py | 25 +++++++++++++-------- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index e70bb9a1a2..edae626aad 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -2706,7 +2706,8 @@ def __init__( self.filepath = path super().__init__(**kwargs) - + self._multipliers: np.ndarray | None = None + self._scalings: np.ndarray | None = None self._log_array: np.ndarray | None = None @property @@ -2738,10 +2739,7 @@ def append_sub_indices(elements, header): def initialize(self): self.last_beta = self.invProb.beta - self.multipliers = self.invProb.dmisfit.multipliers - self.scalings = np.ones_like(self.multipliers) # Everyone gets a fair chance self.misfit_tree_indices = self.parse_by_nested_levels(self.nesting) - self.write_log() def scale_by_level( @@ -2890,9 +2888,39 @@ def write_log(self): f, self.log_array, header="Iterations - Scaling per misfit", - fmt=["%d"] + ["%0.2e"] * (len(self._log_array.dtype) - 1), + fmt=["%d"] + ["%0.3e"] * (len(self._log_array.dtype) - 1), ) + @property + def multipliers(self) -> np.ndarray: + """Static misfit multipliers set at the start of the inversion.""" + if self._multipliers is None: + self._multipliers = self.invProb.dmisfit.multipliers + + return self._multipliers + + @multipliers.setter + def multipliers(self, value): + if not isinstance(value, np.ndarray): + raise TypeError("Multipliers must be a numpy array.") + self._multipliers = value + + @property + def scalings(self) -> np.ndarray: + """Get the current scaling factors.""" + if self._scalings is None: + self._scalings = np.ones_like( + self.invProb.dmisfit.multipliers + ) # Everyone gets a fair chance + + return self._scalings + + @scalings.setter + def scalings(self, value): + if not isinstance(value, np.ndarray): + raise TypeError("Scaling factors must be a numpy array.") + self._scalings = value + def compute_JtJdiags(data_misfit, m): if hasattr(data_misfit, "getJtJdiag"): diff --git a/simpeg/directives/_save_geoh5.py b/simpeg/directives/_save_geoh5.py index 9a5c7fd32e..9a2691f489 100644 --- a/simpeg/directives/_save_geoh5.py +++ b/simpeg/directives/_save_geoh5.py @@ -280,25 +280,32 @@ def write(self, iteration: int, values: list[np.ndarray] = None): # flake8: noq channel_name, base_name = self.get_names( component, label, iteration ) - + data_type = self.data_type[component].get(ii, None) data = h5_object.add_data( { channel_name: { "association": self.association, "values": values, + "entity_type": data_type, } } ) - # Re-assign the data type - if ii not in self.data_type[component].keys(): + if data_type is None: + type_name = f"{self._attribute_type}" + + if component: + type_name += f"_{component}" + + if label: + type_name += f"_{label}" + self.data_type[component][ii] = data.entity_type - type_name = f"{self._attribute_type}_{component}" + f"_{label}" data.entity_type.name = type_name - else: - data.entity_type = w_s.find_type( - self.data_type[component][ii].uid, - type(self.data_type[component][ii]), - ) + + # data.entity_type = w_s.find_type( + # self.data_type[component][ii].uid, + # type(self.data_type[component][ii]), + # ) class SaveModelGeoH5(SaveArrayGeoH5): From 1f22857226a3892fe943fb90a4d895fe0cc349de Mon Sep 17 00:00:00 2001 From: domfournier Date: Tue, 30 Jun 2026 12:55:43 -0700 Subject: [PATCH 5/6] Adapt more directives for restart --- simpeg/directives/_directives.py | 14 ++++++++++---- simpeg/directives/_sim_directives.py | 8 +++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index edae626aad..0b00ec29c2 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -2691,11 +2691,13 @@ def __init__( path: pathlib.Path | None = None, nesting: list[list] | None = None, target_chi: float = 1.0, + cooling_factor: float = 2.0, headers: list[str] | None = None, **kwargs, ): - self.last_beta = None + self.chi_factors = None + self.cooling_factor = cooling_factor self.target_chi = target_chi self.nesting = nesting self.headers = headers @@ -2706,6 +2708,7 @@ def __init__( self.filepath = path super().__init__(**kwargs) + self._last_beta = None self._multipliers: np.ndarray | None = None self._scalings: np.ndarray | None = None self._log_array: np.ndarray | None = None @@ -2738,7 +2741,6 @@ def append_sub_indices(elements, header): return self._log_array def initialize(self): - self.last_beta = self.invProb.beta self.misfit_tree_indices = self.parse_by_nested_levels(self.nesting) self.write_log() @@ -2804,7 +2806,11 @@ def scale_by_level( return scaling_vector def endIter(self): - ratio = self.invProb.beta / self.last_beta + if self._last_beta is None: + ratio = 1.0 / self.cooling_factor + else: + ratio = self.invProb.beta / self._last_beta + nested_residuals = self.parse_by_nested_levels( self.nesting, self.invProb.residuals ) @@ -2818,7 +2824,7 @@ def endIter(self): # Normalize total phi_d with scalings self.invProb.dmisfit.multipliers = self.multipliers * self.scalings - self.last_beta = self.invProb.beta + self._last_beta = self.invProb.beta # Log the scaling factors self.write_log() diff --git a/simpeg/directives/_sim_directives.py b/simpeg/directives/_sim_directives.py index c5144a7707..7d4655ac24 100644 --- a/simpeg/directives/_sim_directives.py +++ b/simpeg/directives/_sim_directives.py @@ -397,9 +397,15 @@ def endIter(self): """ End of iteration update. """ + self.scale_on_current_model(self.opt.xc) + + def scale_on_current_model(self, model): + """ + Scale the cross-gradient term based on the current iteration. + """ max_deriv = [] for _, wire in self.cross_gradient.wire_map.maps: - component = wire * self.opt.xc + component = wire * model max_deriv.append( (component.max() - component.min()) / self.cross_gradient.regularization_mesh.base_length**2.0 From b0bd5f52e63a1ab957b1d9c833cc2b514a223210 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 6 Jul 2026 12:19:38 -0700 Subject: [PATCH 6/6] Clean up --- simpeg/directives/_save_geoh5.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/simpeg/directives/_save_geoh5.py b/simpeg/directives/_save_geoh5.py index 9a2691f489..d05e4642a5 100644 --- a/simpeg/directives/_save_geoh5.py +++ b/simpeg/directives/_save_geoh5.py @@ -302,11 +302,6 @@ def write(self, iteration: int, values: list[np.ndarray] = None): # flake8: noq self.data_type[component][ii] = data.entity_type data.entity_type.name = type_name - # data.entity_type = w_s.find_type( - # self.data_type[component][ii].uid, - # type(self.data_type[component][ii]), - # ) - class SaveModelGeoH5(SaveArrayGeoH5): """