• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rgerum / saenopy / 30918277545

04 Aug 2026 02:18PM UTC coverage: 61.015% (-0.02%) from 61.038%
30918277545

push

github

rgerum
PROTOTYPE: make the cell image toggle actually toggle

Two problems with the control, neither in the viewer itself.

It was a select that already read 'on', and picking the value it already has
fires no change event, so clicking it looked broken. It is a checkbox now,
where the state is visible and either direction fires.

The assets are served without cache headers and saenopy_viewer.mjs changed
several times under the same URL, so a browser that had the page open could
still be running a build from before floor_image existed, where the handler
had nothing to toggle. All three asset URLs now carry a version query.

Verified by driving the checkbox both ways and comparing the screenshots: off
differs from on, and switching back reproduces the original pixel for pixel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

8173 of 13395 relevant lines covered (61.02%)

0.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.96
/saenopy/gui/solver/modules/Regularizer.py
1
import os
1✔
2
import time
1✔
3
from qtpy import QtCore, QtWidgets
1✔
4
import qtawesome as qta
1✔
5
import numpy as np
1✔
6
from typing import Tuple
1✔
7
from pathlib import Path
1✔
8

9
import saenopy
1✔
10
import saenopy.multigrid_helper
1✔
11
from saenopy import Result
1✔
12
import saenopy.get_deformations
1✔
13
import saenopy.materials
1✔
14
from saenopy.gui.common import QtShortCuts
1✔
15
from saenopy.gui.common.gui_classes import CheckAbleGroup, MatplotlibWidget
1✔
16

17
from saenopy.gui.common.PipelineModule import PipelineModule, StateEnum
1✔
18
from saenopy.gui.common.code_export import get_code, export_as_string
1✔
19

20
import matplotlib.ticker as ticker
1✔
21

22
class OmitLast30PercentLocator(ticker.AutoLocator):
1✔
23
    def __call__(self):
1✔
24
        ticks = super(OmitLast30PercentLocator, self).__call__()
1✔
25
        # Safely fetch the axis limits without modifying them
26
        lim = self.axis.get_view_interval()
1✔
27
        cutoff = lim[0] + (lim[1] - lim[0]) * 0.7
1✔
28
        return [t for t in ticks if t < cutoff]
1✔
29

30

31
class CancelSignal:
1✔
32
    cancel = False
1✔
33

34

35
class Regularizer(PipelineModule):
1✔
36
    pipeline_name = "fit forces"
1✔
37
    iteration_finished = QtCore.Signal(object, object, int, int)
1✔
38

39
    pipeline_allow_cancel = True
1✔
40
    pipeline_button_name = "calculate forces"
1✔
41

42
    def __init__(self, parent: "BatchEvaluate", layout):
1✔
43
        super().__init__(parent, layout)
1✔
44

45
        with QtShortCuts.QVBoxLayout(self) as layout:
1✔
46
            layout.setContentsMargins(0, 0, 0, 0)
1✔
47
            with CheckAbleGroup(self, "fit forces (regularize)", url="https://saenopy.readthedocs.io/en/latest/interface_solver.html#fit-deformations-and-calculate-forces").addToLayout() as self.group:
1✔
48

49
                with QtShortCuts.QVBoxLayout() as main_layout:
1✔
50
                    with QtShortCuts.QGroupBox(None, "Material Parameters") as self.material_parameters:
1✔
51
                        with QtShortCuts.QHBoxLayout() as layout2:
1✔
52
                            self.input_k = QtShortCuts.QInputString(None, "k", "1645", type=float, tooltip="the stiffness of the material's fibers")
1✔
53
                            self.input_d_0 = QtShortCuts.QInputString(None, "d_0", "0.0008", type=float, tooltip="the bluckling strength of the material's fibers")
1✔
54
                            self.input_lamda_s = QtShortCuts.QInputString(None, "λ_s", "0.0075", type=float, tooltip="the length at which strain stiffening of the material's fibers starts")
1✔
55
                            self.input_d_s = QtShortCuts.QInputString(None, "d_s", "0.033", type=float, tooltip="the strain stiffening strength of the material's fibers")
1✔
56

57
                    with QtShortCuts.QGroupBox(None, "Regularisation Parameters") as self.material_parameters:
1✔
58
                        self.input_previous_t_as_start = QtShortCuts.QInputBool(None, "use previous time steps deformation field", True,
1✔
59
                                                                tooltip="wether to use the previous time steps deformation field as a starting value for the next regularisation")
60
                        with QtShortCuts.QHBoxLayout(None) as layout:
1✔
61
                            self.input_alpha = QtShortCuts.QInputString(None, "alpha", "1e10", type="exp", tooltip="the strength of the regularisation (higher values mean weaker forces)")
1✔
62
                            self.input_step_size = QtShortCuts.QInputString(None, "step size", "0.33", type=float, tooltip="the step with of the iteration algorithm")
1✔
63
                        with QtShortCuts.QHBoxLayout(None) as layout:
1✔
64
                            self.input_imax = QtShortCuts.QInputNumber(None, "max iterations", 100, float=False, tooltip="the maximum number of iterations after which to abort the iteration algorithm")
1✔
65
                            self.input_conv_crit = QtShortCuts.QInputString(None, "rel. conv. crit.", 0.01, type=float, tooltip="the convergence criterion of the iteration algorithm")
1✔
66

67
                    with QtShortCuts.QHBoxLayout():
1✔
68
                        self.input_button = QtShortCuts.QPushButton(None, "calculate forces", self.start_process,
1✔
69
                                                                    tooltip="run the force calculation")
70
                        self.input_button_text = QtWidgets.QLabel().addToLayout()
1✔
71
                        self.input_button_text.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
1✔
72

73
                        self.input_button_reset = QtShortCuts.QPushButton(None, "", self.reset, icon=qta.icon("fa5s.trash-alt"),
1✔
74
                                                                          tooltip="reset")
75
                        self.input_button_reset.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
1✔
76

77

78
                    self.canvas = MatplotlibWidget(self)
1✔
79
                    self.parent.results_pane.addWidget(QtWidgets.QLabel("convergence of force fit"))
1✔
80
                    self.parent.results_pane.addWidget(self.canvas, 1)
1✔
81
                    #NavigationToolbar(self.canvas, self).addToLayout()
82

83
        self.setParameterMapping("material_parameters", {
1✔
84
            "k": self.input_k,
85
            "d_0": self.input_d_0,
86
            "lambda_s": self.input_lamda_s,
87
            "d_s": self.input_d_s,
88
        })
89
        self.setParameterMapping("solve_parameters", {
1✔
90
            "alpha": self.input_alpha,
91
            "step_size": self.input_step_size,
92
            "max_iterations": self.input_imax,
93
            "rel_conv_crit": self.input_conv_crit,
94
            "prev_t_as_start": self.input_previous_t_as_start,
95
        })
96

97
        self.initialize_plot()
1✔
98
        self.iteration_finished.connect(self.iteration_callback)
1✔
99
        self.iteration_finished.emit(None, np.ones([10, 3]), 0, None)
1✔
100

101
    def cancel_process(self):
1✔
102
        self.set_result_state(self.result, StateEnum.cancelling)
×
103
        self.parent.result_changed.emit(self.result)
×
104

105
        self.cancel_p.cancel = True
×
106

107
    def reset(self):
1✔
108
        if self.result is not None:
×
109
            if self.parent.has_scheduled_tasks():
×
110
                raise ValueError("Tasks are still scheduled")
×
111
            self.result.reset_regularisation_results()
×
112
            self.set_result_state(self.result, StateEnum.idle)
×
113
            self.parent.result_changed.emit(self.result)
×
114

115
    def check_available(self, result: Result):
1✔
116
        if result is None or result.solvers is None:
1✔
117
            return False
×
118
        for solver in result.solvers:
1✔
119
            if solver is None:
1✔
120
                return False
1✔
121
        return True
1✔
122

123
    def check_status(self, result: Result) -> Tuple[str, int, int]:
1✔
124
        if result is None or result.solvers is None:
1✔
125
            return "not-available", 0, 0
1✔
126
        max_count = len(result.solvers)
1✔
127
        count = 0
1✔
128
        for solver in result.solvers:
1✔
129
            relrec = getattr(solver, "regularisation_results", None)
1✔
130
            if relrec is None:
1✔
131
                break
1✔
132
            count += 1
1✔
133
        if count < max_count:
1✔
134
            return "progress", count, max_count
1✔
135
        return "finished", max_count, max_count
1✔
136

137
    def initialize_plot(self):
1✔
138
        self.canvas.figure.axes[0].cla()
1✔
139
        self.canvas_text = self.canvas.figure.axes[0].text(0.5, 0.5, "no fit yet", ha="center",
1✔
140
                                        transform=self.canvas.figure.axes[0].transAxes)
141
        self.canvas_plot = self.canvas.figure.axes[0].semilogy([[0,1]], label="total loss")[0]
1✔
142
        self.canvas.figure.axes[0].spines["top"].set_visible(False)
1✔
143
        self.canvas.figure.axes[0].spines["right"].set_visible(False)
1✔
144

145
        self.canvas.figure.axes[0].text(0, 1, "error  ", ha="right", transform=self.canvas.figure.axes[0].transAxes)
1✔
146
        self.canvas.figure.axes[0].text(1, 0, "\n\niteration", ha="right", va="center",
1✔
147
                                        transform=self.canvas.figure.axes[0].transAxes)
148
        self.canvas.figure.axes[0].xaxis.set_major_locator(OmitLast30PercentLocator())  # Set default automatic locator
1✔
149
        try:
1✔
150
            self.canvas.figure.tight_layout(pad=0)
1✔
151
        except np.linalg.LinAlgError:
×
152
            pass
×
153
        QtCore.QTimer.singleShot(0, self.canvas.draw)
1✔
154

155
    def iteration_callback(self, result, relrec, i=0, imax=None):
1✔
156
        if imax is not None:
1✔
157
            self.parent.progressbar.setRange(0, imax)
1✔
158
            self.parent.progressbar.setValue(i)
1✔
159
        if result is self.result:
1✔
160
            #for i in range(self.parent.tabs.count()):
161
            #    if self.parent.tabs.widget(i) == self.tab.parent():
162
            #        self.parent.tabs.setTabEnabled(i, self.check_evaluated(result))
163
            if self.canvas is not None:
1✔
164
                relrec = np.array(relrec).reshape(-1, 3)
1✔
165
                self.canvas_plot.set_xdata(np.arange(len(relrec[:, 0])))
1✔
166
                self.canvas_plot.set_ydata(relrec[:, 0])
1✔
167
                self.canvas_plot.set_visible(True)
1✔
168
                self.canvas_text.set_visible(False)
1✔
169
                self.canvas.figure.axes[0].set_xlim(0, len(relrec[:, 0])+0.1)
1✔
170

171
                self.canvas.figure.axes[0].relim()  # Recompute limits based on data
1✔
172
                self.canvas.figure.axes[0].autoscale_view()  # Apply updated limits
1✔
173
                try:
1✔
174
                    self.canvas.figure.tight_layout(pad=0)
1✔
175
                except np.linalg.LinAlgError:
×
176
                    pass
×
177
                QtCore.QTimer.singleShot(0, self.canvas.draw_idle)  # Use Qt timer to prevent recursive repaints
1✔
178

179
    def plot_empty(self):
1✔
180
        self.canvas_plot.set_visible(False)
1✔
181
        self.canvas_text.set_visible(True)
1✔
182
        QtCore.QTimer.singleShot(0, self.canvas.draw_idle)
1✔
183

184
    def process(self, result: Result, material_parameters: dict, solve_parameters: dict):
1✔
185
        self.cancel_p = CancelSignal()
1✔
186
        # demo run
187
        if os.environ.get("DEMO") == "true":
1✔
188
            imax = 100
×
189
            self.parent.progressbar.setRange(0, imax)
×
190
            for i in range(len(result.solver_relrec_demo)):
×
191
                time.sleep(0.2)
×
192
                self.iteration_finished.emit(result, result.solver_relrec_demo[:i], i, imax)
×
193
            result.solvers[0].regularisation_results = result.solver_relrec_demo
×
194
            return
×
195

196
        i = 0
1✔
197
        for i in range(len(result.solvers)):
1✔
198
            # if the current is evaluated
199
            if getattr(result.solvers[i], "regularisation_results", None) is not None:
1✔
200
                # and the next one is evaluated
201
                if i < len(result.solvers) - 1 and getattr(result.solvers[i+1], "regularisation_results", None) is not None:
×
202
                    # then skip
203
                    continue
×
204
            self.parent.signal_process_status_update.emit(f"{i}/{len(result.solvers)} fitting forces", f"{Path(result.output).name}")
1✔
205

206
            print(f"Current Timstep: {i}")
1✔
207
            M = result.solvers[i]
1✔
208

209
            if i > 0 and solve_parameters["prev_t_as_start"]:
1✔
210
                M.mesh.displacements[:] = result.solvers[i-1].mesh.displacements.copy()
1✔
211
            if len(result.solvers) == 1 and solve_parameters["prev_t_as_start"]:
1✔
212
                M.mesh.displacements[:] = M.mesh.displacements_target.copy()
1✔
213
                M.mesh.displacements[np.isnan(M.mesh.displacements[:])] = 0
1✔
214

215
            def callback(M, relrec, i, imax):
1✔
216
                self.iteration_finished.emit(result, relrec, i, imax)
1✔
217

218
            M.set_material_model(saenopy.materials.SemiAffineFiberMaterial(
1✔
219
                               material_parameters["k"],
220
                               material_parameters["d_0"] if material_parameters["d_0"] != "None" else None,
221
                               material_parameters["lambda_s"] if material_parameters["lambda_s"] != "None" else None,
222
                               material_parameters["d_s"] if material_parameters["d_s"] != "None" else None,
223
                               ))
224

225
            M.solve_regularized(step_size=solve_parameters["step_size"], max_iterations=solve_parameters["max_iterations"],
1✔
226
                                alpha=solve_parameters["alpha"], rel_conv_crit=solve_parameters["rel_conv_crit"],
227
                                callback=callback, verbose=True, cancel_signal=self.cancel_p)
228

229
            # clear the cache of the solver
230
            result.clear_cache(i)
1✔
231
            result.save()
1✔
232
            self.parent.result_changed.emit(result)
1✔
233

234
            if self.cancel_p.cancel is True:
1✔
235
                return "Terminated"
×
236

237
        self.parent.signal_process_status_update.emit(f"{i+1}/{len(result.solvers)} fitting forces",
1✔
238
                                                      f"{Path(result.output).name}")
239

240
    def setResult(self, result: Result):
1✔
241
        super().setResult(result)
1✔
242
        self.update_plot()
1✔
243

244
    def update_plot(self):
1✔
245
        if self.result.solvers is None or len(self.result.solvers) == 0:
1✔
246
            return
1✔
247
        relrec = getattr(self.result.solvers[self.parent.t_slider.value()], "relrec", None)
1✔
248
        if relrec is None:
1✔
249
            relrec = getattr(self.result.solvers[self.parent.t_slider.value()], "regularisation_results", None)
1✔
250
        if relrec is not None:
1✔
251
            self.iteration_callback(self.result, relrec)
1✔
252
        else:
253
            self.plot_empty()
1✔
254

255
    def get_code(self) -> Tuple[str, str]:
1✔
256
        import_code = "import saenopy\n"
1✔
257
        results: Result = None
1✔
258

259
        @export_as_string
1✔
260
        def code(my_reg_params1, my_reg_params2):  # pragma: no cover
261
            # define the parameters to generate the solver mesh and interpolate the piv mesh onto it
262
            material_parameters = my_reg_params1
263
            solve_parameters = my_reg_params2
264

265
            # iterate over all the results objects
266
            for result in results:
267
                result.material_parameters = material_parameters
268
                result.solve_parameters = solve_parameters
269
                for index, M in enumerate(result.solvers):
270
                    # optionally copy the displacement field from the previous time step as a starting value
271
                    if index > 0 and solve_parameters["prev_t_as_start"]:
272
                        M.mesh.displacements[:] = result.solvers[index - 1].mesh.displacements.copy()
273

274
                    # set the material model
275
                    M.set_material_model(saenopy.materials.SemiAffineFiberMaterial(
276
                        material_parameters["k"],
277
                        material_parameters["d_0"],
278
                        material_parameters["lambda_s"],
279
                        material_parameters["d_s"],
280
                    ))
281
                    # find the regularized force solution
282
                    M.solve_regularized(alpha=solve_parameters["alpha"], step_size=solve_parameters["step_size"],
283
                                        max_iterations=solve_parameters["max_iterations"], rel_conv_crit=solve_parameters["rel_conv_crit"],
284
                                        verbose=True)
285
                    # save the forces
286
                    result.save()
287
                    # clear the cache of the solver
288
                    result.clear_cache(index)
289

290
        # params with convert text Nones to real Nones
291
        data = {
1✔
292
            "my_reg_params1": {k: None if v == "None" else v for k, v in self.result.material_parameters_tmp.items()},
293
            "my_reg_params2": {k: None if v == "None" else v for k, v in self.result.solve_parameters_tmp.items()},
294
        }
295

296
        code = get_code(code, data)
1✔
297
        return import_code, code
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc