Show a busy window while the solve runs
On OK the dialog closed and nothing appeared until the figures did, which on a real board is minutes of looking like the plugin did nothing. Put a small always-on-top window up for that stretch: the stage now running, elapsed seconds, and Cancel. Qt only repaints while the event loop runs and the solve owns the thread, so the window pumps events itself - from inside the CG/AMG iteration callback, which is where the time actually goes. That is also where Cancel is noticed. The state is module-level because the tick happens several frames deep in scipy/pyamg, and threading a handle through those signatures for a progress bar is not worth it; it stays inert until start(), so the standalone runner and the tests are unaffected. The window covers the figure work too, not just the solve: laying out labels and writing four PNGs at full DPI is seconds on a real board - 10-15 of them on a large one - and closing before that left the same silent gap one step later.
This commit is contained in:
@@ -34,7 +34,7 @@ import numpy as np
|
|||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
from scipy.sparse import csgraph
|
from scipy.sparse import csgraph
|
||||||
|
|
||||||
from . import config, quadtree, skin
|
from . import config, progress, quadtree, skin
|
||||||
from . import solver as sv
|
from . import solver as sv
|
||||||
from .errors import ConnectivityError
|
from .errors import ConnectivityError
|
||||||
from .geometry import Problem
|
from .geometry import Problem
|
||||||
@@ -300,9 +300,11 @@ def run_solve_adaptive(problem: Problem, stack: RasterStack,
|
|||||||
corr = np.zeros(len(edges.a))
|
corr = np.zeros(len(edges.a))
|
||||||
faces = e_axis >= 0
|
faces = e_axis >= 0
|
||||||
fa, fb = edges.a[faces], edges.b[faces]
|
fa, fb = edges.a[faces], edges.b[faces]
|
||||||
for _ in range(max(0, int(config.ADAPTIVE_CORRECTION_PASSES))):
|
passes = max(0, int(config.ADAPTIVE_CORRECTION_PASSES))
|
||||||
|
for p in range(passes):
|
||||||
if not faces.any():
|
if not faces.any():
|
||||||
break
|
break
|
||||||
|
progress.stage(f"correction pass {p + 1}/{passes} ...")
|
||||||
gx, gy = _leaf_gradients(N, fa, fb, cxg, cyg, Vflat)
|
gx, gy = _leaf_gradients(N, fa, fb, cxg, cyg, Vflat)
|
||||||
gt = np.where(e_axis[faces] == 0, 0.5 * (gy[fa] + gy[fb]),
|
gt = np.where(e_axis[faces] == 0, 0.5 * (gy[fa] + gy[fb]),
|
||||||
0.5 * (gx[fa] + gx[fb]))
|
0.5 * (gx[fa] + gx[fb]))
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from . import config, pipeline, report
|
from . import config, pipeline, progress, report
|
||||||
from .errors import CandidateError, UserFacingError
|
from .errors import CandidateError, UserFacingError
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +89,9 @@ def main() -> None:
|
|||||||
if selection is None:
|
if selection is None:
|
||||||
print("cancelled")
|
print("cancelled")
|
||||||
return
|
return
|
||||||
|
# the solve owns the thread from here; without this the plugin
|
||||||
|
# looks like it did nothing until the figures appear
|
||||||
|
progress.start()
|
||||||
|
|
||||||
if selection.contact1 != "auto":
|
if selection.contact1 != "auto":
|
||||||
for e in es1:
|
for e in es1:
|
||||||
@@ -122,10 +125,14 @@ def main() -> None:
|
|||||||
freq_hz=selection.freq_hz,
|
freq_hz=selection.freq_hz,
|
||||||
contact_model=selection.contact_model,
|
contact_model=selection.contact_model,
|
||||||
overlay=overlay_cb)
|
overlay=overlay_cb)
|
||||||
|
except progress.Cancelled:
|
||||||
|
print("cancelled") # user's own doing: no error figure
|
||||||
except UserFacingError as e:
|
except UserFacingError as e:
|
||||||
_fail(str(e), outdir)
|
_fail(str(e), outdir)
|
||||||
except Exception:
|
except Exception:
|
||||||
_fail(traceback.format_exc(), outdir)
|
_fail(traceback.format_exc(), outdir)
|
||||||
|
finally:
|
||||||
|
progress.done() # also on the error paths
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import config, plots, raster, report, solver
|
from . import config, plots, progress, raster, report, solver
|
||||||
from .errors import UserFacingError
|
from .errors import UserFacingError
|
||||||
from .geometry import Problem
|
from .geometry import Problem
|
||||||
from .solver import Result
|
from .solver import Result
|
||||||
@@ -20,8 +20,8 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
|||||||
if i_test <= 0:
|
if i_test <= 0:
|
||||||
raise UserFacingError(f"Test current must be > 0 A (got {i_test:g}).")
|
raise UserFacingError(f"Test current must be > 0 A (got {i_test:g}).")
|
||||||
h = raster.choose_cell_size(problem.copper_bbox(), len(problem.layers))
|
h = raster.choose_cell_size(problem.copper_bbox(), len(problem.layers))
|
||||||
print(f"rasterizing {len(problem.layers)} layer(s) at cell size "
|
progress.stage(f"rasterizing {len(problem.layers)} layer(s) at cell "
|
||||||
f"{h / 1000:.1f} um ...")
|
f"size {h / 1000:.1f} um ...")
|
||||||
stack = raster.rasterize_stack(problem, h)
|
stack = raster.rasterize_stack(problem, h)
|
||||||
print(f"grid {stack.shape2d[1]}x{stack.shape2d[0]}x{stack.nlayers}, "
|
print(f"grid {stack.shape2d[1]}x{stack.shape2d[0]}x{stack.nlayers}, "
|
||||||
f"{int(stack.masks.sum())} copper cells, {len(problem.vias)} "
|
f"{int(stack.masks.sum())} copper cells, {len(problem.vias)} "
|
||||||
@@ -30,7 +30,7 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
|||||||
e1, e2 = raster.electrode_masks(stack, problem)
|
e1, e2 = raster.electrode_masks(stack, problem)
|
||||||
parts1, parts2 = raster.electrode_partition(stack, problem)
|
parts1, parts2 = raster.electrode_partition(stack, problem)
|
||||||
|
|
||||||
print(f"solving @ {i_test:g} A"
|
progress.stage(f"solving @ {i_test:g} A"
|
||||||
+ (f", {freq_hz:g} Hz" if freq_hz > 0 else " DC") + " ...")
|
+ (f", {freq_hz:g} Hz" if freq_hz > 0 else " DC") + " ...")
|
||||||
result = solver.run_solve(problem, stack, e1, e2, i_test, freq_hz,
|
result = solver.run_solve(problem, stack, e1, e2, i_test, freq_hz,
|
||||||
contact_model, parts1, parts2)
|
contact_model, parts1, parts2)
|
||||||
@@ -51,6 +51,7 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"overlay push failed: {e}")
|
print(f"overlay push failed: {e}")
|
||||||
|
|
||||||
|
progress.stage("rendering figures ...")
|
||||||
figs = [
|
figs = [
|
||||||
(plots.fig_raster(stack, e1, e2, problem, result), "1_raster_map"),
|
(plots.fig_raster(stack, e1, e2, problem, result), "1_raster_map"),
|
||||||
(plots.fig_potential(result, stack, e1, e2, problem), "2_potential"),
|
(plots.fig_potential(result, stack, e1, e2, problem), "2_potential"),
|
||||||
@@ -58,5 +59,5 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
|||||||
"3_current_density"),
|
"3_current_density"),
|
||||||
(plots.fig_power(result, stack, e1, e2, problem), "4_power_density"),
|
(plots.fig_power(result, stack, e1, e2, problem), "4_power_density"),
|
||||||
]
|
]
|
||||||
plots.save_and_show(figs, outdir, show=show)
|
plots.save_and_show(figs, outdir, show=show) # closes the window itself
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from matplotlib.gridspec import GridSpec # noqa: E402
|
|||||||
from matplotlib.patches import Patch # noqa: E402
|
from matplotlib.patches import Patch # noqa: E402
|
||||||
from matplotlib.widgets import CheckButtons # noqa: E402
|
from matplotlib.widgets import CheckButtons # noqa: E402
|
||||||
|
|
||||||
from . import config # noqa: E402
|
from . import config, progress # noqa: E402
|
||||||
|
|
||||||
_BG = "#f5f3f0"
|
_BG = "#f5f3f0"
|
||||||
_COPPER = "#c98b4e"
|
_COPPER = "#c98b4e"
|
||||||
@@ -514,11 +514,15 @@ def save_and_show(figs_named: list[tuple], outdir: Path | None,
|
|||||||
show: bool = True) -> list[Path]:
|
show: bool = True) -> list[Path]:
|
||||||
"""figs_named: [(figure, basename), ...]. Saves first, then shows."""
|
"""figs_named: [(figure, basename), ...]. Saves first, then shows."""
|
||||||
saved = []
|
saved = []
|
||||||
|
progress.stage("laying out figures ...", echo=False)
|
||||||
for fig, _ in figs_named:
|
for fig, _ in figs_named:
|
||||||
_resolve_label_overlaps(fig)
|
_resolve_label_overlaps(fig)
|
||||||
if outdir is not None:
|
if outdir is not None:
|
||||||
outdir.mkdir(parents=True, exist_ok=True)
|
outdir.mkdir(parents=True, exist_ok=True)
|
||||||
for fig, name in figs_named:
|
for fig, name in figs_named:
|
||||||
|
# full-DPI savefig with tight bounding boxes is seconds per
|
||||||
|
# figure - the progress window has to stay up for it
|
||||||
|
progress.stage(f"saving {name}.png ...", echo=False)
|
||||||
panel = getattr(fig, "_layer_panel", None)
|
panel = getattr(fig, "_layer_panel", None)
|
||||||
if panel is not None:
|
if panel is not None:
|
||||||
panel.set_visible(False) # PNGs carry no checkboxes
|
panel.set_visible(False) # PNGs carry no checkboxes
|
||||||
@@ -531,13 +535,18 @@ def save_and_show(figs_named: list[tuple], outdir: Path | None,
|
|||||||
print(f"saved {p}")
|
print(f"saved {p}")
|
||||||
if show and config.INTERACTIVE:
|
if show and config.INTERACTIVE:
|
||||||
if INTERACTIVE_BACKEND:
|
if INTERACTIVE_BACKEND:
|
||||||
|
progress.stage("opening the figure windows ...", echo=False)
|
||||||
for fig, _ in figs_named:
|
for fig, _ in figs_named:
|
||||||
_fit_to_screen(fig)
|
_fit_to_screen(fig)
|
||||||
|
progress.done() # last thing before the figures are up
|
||||||
_raise_windows()
|
_raise_windows()
|
||||||
plt.show()
|
plt.show()
|
||||||
else:
|
else:
|
||||||
|
progress.done()
|
||||||
for p in saved:
|
for p in saved:
|
||||||
_open_in_viewer(p)
|
_open_in_viewer(p)
|
||||||
|
else:
|
||||||
|
progress.done()
|
||||||
plt.close("all")
|
plt.close("all")
|
||||||
return saved
|
return saved
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""Busy window for the stretch between the dialog closing and the
|
||||||
|
figures appearing.
|
||||||
|
|
||||||
|
The solve is seconds to minutes on a real board, and until now nothing
|
||||||
|
was on screen for it: the dialog vanished on OK and the plugin looked
|
||||||
|
like it had done nothing. This puts a small always-on-top window up for
|
||||||
|
that stretch - current stage, elapsed time, and a Cancel button.
|
||||||
|
|
||||||
|
The state is module-level rather than an object threaded through the
|
||||||
|
call chain: the linear solve is where the time actually goes, and it
|
||||||
|
calls tick() from inside a scipy/pyamg iteration callback several
|
||||||
|
frames deep. Inactive until start() succeeds, so every call is a no-op
|
||||||
|
for the standalone runner and the tests.
|
||||||
|
|
||||||
|
Qt only repaints when the event loop runs, and the solve owns the
|
||||||
|
thread, so tick() pumps events itself. That is also where a click on
|
||||||
|
Cancel is noticed - it raises Cancelled at the next tick.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
_win = None
|
||||||
|
_label = None
|
||||||
|
_text = ""
|
||||||
|
_t0 = 0.0
|
||||||
|
_last = 0.0
|
||||||
|
_cancelled = False
|
||||||
|
|
||||||
|
TICK_INTERVAL_S = 0.05 # ~20 fps: enough to look alive, cheap
|
||||||
|
|
||||||
|
|
||||||
|
class Cancelled(Exception):
|
||||||
|
"""The user closed the progress window. Not a failure - the caller
|
||||||
|
reports it like a cancelled dialog, with no error figure."""
|
||||||
|
|
||||||
|
|
||||||
|
def start(title: str = "Fill Resistance") -> bool:
|
||||||
|
"""Show the window. False (and inert) if Qt is unavailable."""
|
||||||
|
global _win, _label, _t0, _last, _cancelled, _text
|
||||||
|
if _win is not None:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtWidgets import (QApplication, QDialog,
|
||||||
|
QDialogButtonBox, QLabel,
|
||||||
|
QProgressBar, QVBoxLayout)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
app = QApplication.instance() or QApplication([])
|
||||||
|
win = QDialog()
|
||||||
|
win.setWindowTitle(title)
|
||||||
|
win.setWindowFlag(Qt.WindowStaysOnTopHint, True)
|
||||||
|
# no close button: closing is Cancel, and Cancel is the only way
|
||||||
|
# to stop a solve that owns the thread
|
||||||
|
win.setWindowFlag(Qt.WindowCloseButtonHint, False)
|
||||||
|
|
||||||
|
label = QLabel("starting ...")
|
||||||
|
bar = QProgressBar()
|
||||||
|
bar.setRange(0, 0) # indeterminate: no total to show
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.addWidget(label)
|
||||||
|
layout.addWidget(bar)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
win.setLayout(layout)
|
||||||
|
|
||||||
|
buttons.rejected.connect(_cancel)
|
||||||
|
win.rejected.connect(_cancel)
|
||||||
|
win.setMinimumWidth(340)
|
||||||
|
win.show()
|
||||||
|
win.raise_()
|
||||||
|
win.activateWindow()
|
||||||
|
app.processEvents()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
_win, _label, _t0, _last, _cancelled, _text = win, label, \
|
||||||
|
time.monotonic(), 0.0, False, ""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _cancel() -> None:
|
||||||
|
global _cancelled
|
||||||
|
_cancelled = True
|
||||||
|
|
||||||
|
|
||||||
|
def stage(text: str, echo: bool = True) -> None:
|
||||||
|
"""Name the phase now running. Always repaints - stages are rare.
|
||||||
|
|
||||||
|
echo=False for phases that already print their own line (saving a
|
||||||
|
PNG prints the path), so the window updates without doubling stdout.
|
||||||
|
"""
|
||||||
|
global _text
|
||||||
|
_text = text
|
||||||
|
if echo:
|
||||||
|
print(text)
|
||||||
|
if _win is not None:
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def tick() -> None:
|
||||||
|
"""Called from inside the solve. Throttled, so it is safe to call
|
||||||
|
every iteration."""
|
||||||
|
global _last
|
||||||
|
if _win is None:
|
||||||
|
return
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - _last < TICK_INTERVAL_S:
|
||||||
|
return
|
||||||
|
_last = now
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh() -> None:
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
elapsed = time.monotonic() - _t0
|
||||||
|
if _label is not None:
|
||||||
|
_label.setText(f"{_text}\n{elapsed:.0f} s elapsed")
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app is not None:
|
||||||
|
app.processEvents()
|
||||||
|
if _cancelled:
|
||||||
|
raise Cancelled()
|
||||||
|
|
||||||
|
|
||||||
|
def done() -> None:
|
||||||
|
"""Take the window down. Idempotent - callers use it in a finally."""
|
||||||
|
global _win, _label, _text, _cancelled
|
||||||
|
win, _win, _label, _text = _win, None, None, ""
|
||||||
|
_cancelled = False
|
||||||
|
if win is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
win.close()
|
||||||
|
win.deleteLater()
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app is not None:
|
||||||
|
app.processEvents()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -45,7 +45,7 @@ from scipy import sparse
|
|||||||
from scipy.sparse import csgraph
|
from scipy.sparse import csgraph
|
||||||
from scipy.sparse import linalg as sla
|
from scipy.sparse import linalg as sla
|
||||||
|
|
||||||
from . import config, skin
|
from . import config, progress, skin
|
||||||
from .errors import ConnectivityError, ElectrodeError, SolverError
|
from .errors import ConnectivityError, ElectrodeError, SolverError
|
||||||
from .geometry import Problem, slot_distance
|
from .geometry import Problem, slot_distance
|
||||||
from .raster import RasterStack, electrodes_touch
|
from .raster import RasterStack, electrodes_touch
|
||||||
@@ -375,12 +375,14 @@ class PreparedSolver:
|
|||||||
|
|
||||||
def solve(self, b: np.ndarray) -> tuple[np.ndarray, SolveInfo]:
|
def solve(self, b: np.ndarray) -> tuple[np.ndarray, SolveInfo]:
|
||||||
if self._lu is not None:
|
if self._lu is not None:
|
||||||
|
progress.tick() # direct solve: one shot, no iterations
|
||||||
return self._lu.solve(b), SolveInfo(method="spsolve",
|
return self._lu.solve(b), SolveInfo(method="spsolve",
|
||||||
n_unknowns=self.n)
|
n_unknowns=self.n)
|
||||||
if self._ml is not None:
|
if self._ml is not None:
|
||||||
residuals: list[float] = []
|
residuals: list[float] = []
|
||||||
x = self._ml.solve(b, tol=config.AMG_TOL, maxiter=300,
|
x = self._ml.solve(b, tol=config.AMG_TOL, maxiter=300,
|
||||||
accel="cg", residuals=residuals)
|
accel="cg", residuals=residuals,
|
||||||
|
callback=lambda _: progress.tick())
|
||||||
res = float(np.linalg.norm(b - self._A @ x)
|
res = float(np.linalg.norm(b - self._A @ x)
|
||||||
/ max(np.linalg.norm(b), 1e-300))
|
/ max(np.linalg.norm(b), 1e-300))
|
||||||
if not np.isfinite(res) or res > 1e-6:
|
if not np.isfinite(res) or res > 1e-6:
|
||||||
@@ -404,7 +406,7 @@ def _solve_amg(A: sparse.csr_matrix, b: np.ndarray) -> tuple[np.ndarray, SolveIn
|
|||||||
ml = pyamg.smoothed_aggregation_solver(A.tocsr(), max_coarse=500)
|
ml = pyamg.smoothed_aggregation_solver(A.tocsr(), max_coarse=500)
|
||||||
residuals: list[float] = []
|
residuals: list[float] = []
|
||||||
x = ml.solve(b, tol=config.AMG_TOL, maxiter=300, accel="cg",
|
x = ml.solve(b, tol=config.AMG_TOL, maxiter=300, accel="cg",
|
||||||
residuals=residuals)
|
residuals=residuals, callback=lambda _: progress.tick())
|
||||||
res = float(np.linalg.norm(b - A @ x) / max(np.linalg.norm(b), 1e-300))
|
res = float(np.linalg.norm(b - A @ x) / max(np.linalg.norm(b), 1e-300))
|
||||||
if not np.isfinite(res) or res > 1e-6:
|
if not np.isfinite(res) or res > 1e-6:
|
||||||
raise SolverError(
|
raise SolverError(
|
||||||
@@ -428,6 +430,7 @@ def _solve_cg_jacobi(A: sparse.csr_matrix, b: np.ndarray) -> tuple[np.ndarray, S
|
|||||||
def count(_):
|
def count(_):
|
||||||
nonlocal iters
|
nonlocal iters
|
||||||
iters += 1
|
iters += 1
|
||||||
|
progress.tick()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
x, code = sla.cg(A, b, M=M, rtol=config.CG_TOL,
|
x, code = sla.cg(A, b, M=M, rtol=config.CG_TOL,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import argparse
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import config, pipeline
|
from . import config, pipeline, progress
|
||||||
from .errors import UserFacingError
|
from .errors import UserFacingError
|
||||||
from .geometry import load_problem
|
from .geometry import load_problem
|
||||||
from .skin import parse_frequency
|
from .skin import parse_frequency
|
||||||
@@ -51,6 +51,9 @@ def main(argv=None) -> int:
|
|||||||
ap.add_argument("--force-iterative", action="store_true",
|
ap.add_argument("--force-iterative", action="store_true",
|
||||||
help="use the iterative solver (AMG-CG, or Jacobi-CG "
|
help="use the iterative solver (AMG-CG, or Jacobi-CG "
|
||||||
"without pyamg) regardless of problem size")
|
"without pyamg) regardless of problem size")
|
||||||
|
ap.add_argument("--progress", action="store_true",
|
||||||
|
help="show the busy window during the solve, as the "
|
||||||
|
"KiCad plugin does (needs a GUI)")
|
||||||
ap.add_argument("--adaptive", action=argparse.BooleanOptionalAction,
|
ap.add_argument("--adaptive", action=argparse.BooleanOptionalAction,
|
||||||
default=None,
|
default=None,
|
||||||
help="adaptive quadtree grid (coarse plane interiors); "
|
help="adaptive quadtree grid (coarse plane interiors); "
|
||||||
@@ -86,13 +89,20 @@ def main(argv=None) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
outdir = args.out if args.out is not None else args.dump.parent
|
outdir = args.out if args.out is not None else args.dump.parent
|
||||||
|
if args.progress:
|
||||||
|
progress.start()
|
||||||
try:
|
try:
|
||||||
pipeline.run(problem, outdir, show=not args.no_show,
|
pipeline.run(problem, outdir, show=not args.no_show,
|
||||||
i_test=args.current, freq_hz=args.freq,
|
i_test=args.current, freq_hz=args.freq,
|
||||||
contact_model=args.contact_model)
|
contact_model=args.contact_model)
|
||||||
|
except progress.Cancelled:
|
||||||
|
print("cancelled")
|
||||||
|
return 1
|
||||||
except UserFacingError as e:
|
except UserFacingError as e:
|
||||||
print(f"ERROR: {e}", file=sys.stderr)
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
finally:
|
||||||
|
progress.done()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user