优化部分多点标定功能
This commit is contained in:
@@ -354,6 +354,8 @@
|
||||
"start": "Start acquisition",
|
||||
"confirm": "At target force, start hold",
|
||||
"liftoff": "Confirm the indenter has left the sensor",
|
||||
"remeasure": "Measure this step again",
|
||||
"remeasure_ready": "This step was discarded and progress stays on it. Reconnect the sensor, wait until the trace is back near its previous baseline, type this step's reference force, then click At target force, start hold.",
|
||||
"stop": "Stop and keep data",
|
||||
"stopped_reason": "operator stopped",
|
||||
"finger_busy": "A single-point trial is running; finish it before multi-point acquisition",
|
||||
|
||||
@@ -354,6 +354,8 @@
|
||||
"start": "开始采集",
|
||||
"confirm": "已到目标力,开始保持",
|
||||
"liftoff": "确认压头已脱离传感器",
|
||||
"remeasure": "重测本档",
|
||||
"remeasure_ready": "这一档已作废,进度停在同一档。接上传感器,等曲线回到断线前靠近零的位置,人工参考力填这一档的力,再点「已到目标力,开始保持」。",
|
||||
"stop": "停止并保留数据",
|
||||
"stopped_reason": "操作员停止",
|
||||
"finger_busy": "单点试验进行中,请先结束再开始多点采集",
|
||||
|
||||
@@ -133,8 +133,16 @@ _COMPONENT_SETS = (
|
||||
("N", ("FN_N", "FX_N", "FY_N")),
|
||||
("raw", ("FN_raw", "FX_raw", "FY_raw")),
|
||||
)
|
||||
#: Shown in stable_means.csv. Weights are the only load used on this bench.
|
||||
_LOAD_TYPE_LABEL = {
|
||||
"weight": "砝码",
|
||||
"wrench": "六维力",
|
||||
}
|
||||
STABLE_MEANS_HEADER = (
|
||||
"site_id",
|
||||
"U",
|
||||
"V",
|
||||
"负载类型",
|
||||
"repeat_index",
|
||||
"step_index",
|
||||
"branch",
|
||||
@@ -188,36 +196,83 @@ def _without_outliers(values: list[float]) -> list[float]:
|
||||
return kept or [centre]
|
||||
|
||||
|
||||
def stable_component_rows(repeat_dir: Path, *, site_id: str, repeat_index: int) -> list[dict]:
|
||||
def _uv_text(value) -> str:
|
||||
"""One UV coordinate, blank when the point was not picked on the flat map."""
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
if not math.isfinite(number):
|
||||
return ""
|
||||
if number == math.trunc(number):
|
||||
return str(int(number))
|
||||
return f"{number:.6g}"
|
||||
|
||||
|
||||
def _point_uv(repeat_dir: Path) -> tuple[str, str]:
|
||||
"""U and V stored with the repeat. Missing coordinates stay blank."""
|
||||
path = repeat_dir / "session.json"
|
||||
if not path.is_file():
|
||||
return "", ""
|
||||
try:
|
||||
record = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
return "", ""
|
||||
uv = record.get("uv_px") if isinstance(record, dict) else None
|
||||
if not isinstance(uv, dict):
|
||||
return "", ""
|
||||
return _uv_text(uv.get("u")), _uv_text(uv.get("v"))
|
||||
|
||||
|
||||
def stable_component_rows(
|
||||
repeat_dir: Path,
|
||||
*,
|
||||
site_id: str,
|
||||
repeat_index: int,
|
||||
uv: tuple[float, float] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Mean Fn, Fx and Fy over the latter half of each completed hold.
|
||||
|
||||
The first half of a hold is the settling transient. Each component then
|
||||
drops its own outliers before the mean. The file this feeds does not
|
||||
contain a combined force.
|
||||
contain a combined force. U and V are the flat-map coordinates of the
|
||||
point. The load type is the reference used for that hold.
|
||||
"""
|
||||
reference_path = repeat_dir / "reference.csv"
|
||||
if not reference_path.is_file():
|
||||
return []
|
||||
with reference_path.open(encoding="utf-8-sig", newline="") as handle:
|
||||
holds = list(csv.DictReader(handle))
|
||||
stamps: dict[int, int] = {}
|
||||
stamps: list[int | None] = []
|
||||
frames_path = repeat_dir / "capture" / "frames.csv"
|
||||
if frames_path.is_file():
|
||||
with frames_path.open(encoding="utf-8-sig", newline="") as handle:
|
||||
for row in csv.DictReader(handle):
|
||||
if row.get("dropped") not in (None, "", "0") or not row.get("t_host_mono_ns"):
|
||||
# A gap is a hole in the force file, not a sample. A camera that
|
||||
# is plugged back in starts its frame numbers again, so the time
|
||||
# of a sample is the frame written with it, not the frame number.
|
||||
if row.get("dropped") not in (None, "", "0"):
|
||||
continue
|
||||
stamps[int(row["frame_id"])] = int(row["t_host_mono_ns"])
|
||||
text = str(row.get("t_host_mono_ns") or "").strip()
|
||||
stamps.append(int(text) if text else None)
|
||||
samples: list[tuple[int, str, tuple[float, float, float]]] = []
|
||||
forces_path = repeat_dir / "capture" / "forces.csv"
|
||||
if forces_path.is_file():
|
||||
with forces_path.open(encoding="utf-8-sig", newline="") as handle:
|
||||
for row in csv.DictReader(handle):
|
||||
for index, row in enumerate(csv.DictReader(handle)):
|
||||
if index >= len(stamps):
|
||||
break
|
||||
components = _components(row)
|
||||
stamp = stamps.get(int(row["frame_id"])) if row.get("frame_id") else None
|
||||
stamp = stamps[index]
|
||||
if components is None or stamp is None:
|
||||
continue
|
||||
samples.append((stamp, components[0], components[1]))
|
||||
if uv is None:
|
||||
u_text, v_text = _point_uv(repeat_dir)
|
||||
else:
|
||||
u_text, v_text = _uv_text(uv[0]), _uv_text(uv[1])
|
||||
rows = []
|
||||
for hold in holds:
|
||||
if hold.get("hold_complete") not in (None, "", "1"):
|
||||
@@ -230,8 +285,12 @@ def stable_component_rows(repeat_dir: Path, *, site_id: str, repeat_index: int)
|
||||
used = [item[2] for item in chosen if item[1] == unit]
|
||||
cleaned = [_without_outliers(list(axis)) for axis in zip(*used)] if used else []
|
||||
means = tuple(sum(axis) / len(axis) for axis in cleaned) if cleaned else ("", "", "")
|
||||
source = str(hold.get("reference_source") or "").strip()
|
||||
rows.append({
|
||||
"site_id": site_id,
|
||||
"U": u_text,
|
||||
"V": v_text,
|
||||
"负载类型": _LOAD_TYPE_LABEL.get(source, source),
|
||||
"repeat_index": repeat_index,
|
||||
"step_index": hold.get("step_index", ""),
|
||||
"branch": hold.get("branch", ""),
|
||||
@@ -362,6 +421,7 @@ class Campaign:
|
||||
self._rotation: tuple[tuple[float, float, float], ...] | None = None
|
||||
self._pending: dict | None = None
|
||||
self._single_site = False
|
||||
self._finished_step_pending = False
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
@@ -547,6 +607,7 @@ class Campaign:
|
||||
self.simulate = bool(simulate)
|
||||
self.reason = ""
|
||||
self._single_site = False
|
||||
self._finished_step_pending = False
|
||||
self._completed_holds = 0
|
||||
self._trial_serial = 0
|
||||
self.directory = _session_directory(Path(root), self.sensor_model, self.sensor_sn)
|
||||
@@ -597,6 +658,7 @@ class Campaign:
|
||||
if branch == "baseline" and force != 0:
|
||||
raise ValueError("the unloaded baseline needs a reference of 0 when you are using weights")
|
||||
now = time.perf_counter_ns()
|
||||
self._finished_step_pending = False
|
||||
self._pending = {"source": "weight", "force": force, "fx": "", "fy": "", "fz": ""}
|
||||
self._active = (self._step_index, branch, target, force, now)
|
||||
self._hold_started_ns = now
|
||||
@@ -620,6 +682,7 @@ class Campaign:
|
||||
raise ValueError("the normal component is above the maximum force you entered")
|
||||
_branch, _target = self._steps[self._step_index]
|
||||
now = time.perf_counter_ns()
|
||||
self._finished_step_pending = False
|
||||
self._pending = {
|
||||
"source": "wrench",
|
||||
"force": force,
|
||||
@@ -659,8 +722,10 @@ class Campaign:
|
||||
self._completed_holds += 1
|
||||
self._step_index += 1
|
||||
if self._step_index < len(self._steps):
|
||||
self._finished_step_pending = True
|
||||
self.phase = "load"
|
||||
return
|
||||
self._finished_step_pending = False
|
||||
self._close_repeat(complete=True)
|
||||
self._repeat_index += 1
|
||||
if self._single_site:
|
||||
@@ -718,6 +783,72 @@ class Campaign:
|
||||
self._finish_session("incomplete")
|
||||
self.phase = "aborted"
|
||||
|
||||
def can_remeasure_step(self) -> bool:
|
||||
"""True while a hold can be dropped, or the step that just finished can be taken back."""
|
||||
if not self.is_active or self._trial_dir is None:
|
||||
return False
|
||||
if self.phase == "hold":
|
||||
return True
|
||||
return self.phase == "load" and self._step_index > 0 and self._finished_step_pending
|
||||
|
||||
def remeasure_step(self) -> None:
|
||||
"""Discard the hold in progress, or the step that just finished, and wait on that step.
|
||||
|
||||
The discarded window is kept in the file with hold_complete 0, so it is
|
||||
left out of the stable means. The next confirmation records the same
|
||||
step again.
|
||||
"""
|
||||
if not self.can_remeasure_step():
|
||||
raise RuntimeError("this step is already waiting to be measured again")
|
||||
if self.phase == "hold":
|
||||
if self._active is not None and self._reference is not None:
|
||||
step_index, branch, target, force, start = self._active
|
||||
now = time.perf_counter_ns()
|
||||
self._reference.writerow(self._reference_values(
|
||||
step_index, branch, target, start, now, force, 0,
|
||||
))
|
||||
self._reference_file.flush()
|
||||
self._active = None
|
||||
self._pending = None
|
||||
self._hold_started_ns = None
|
||||
self._event("hold_discarded", {"step_index": self._step_index})
|
||||
self.phase = "load"
|
||||
return
|
||||
self._mark_last_hold_incomplete()
|
||||
self._step_index -= 1
|
||||
self._completed_holds = max(0, self._completed_holds - 1)
|
||||
self._finished_step_pending = False
|
||||
self._event("step_remeasured", {"step_index": self._step_index})
|
||||
|
||||
def _mark_last_hold_incomplete(self) -> None:
|
||||
"""The step that just advanced is no longer a finished hold."""
|
||||
if self._trial_dir is None or self._reference_file is None:
|
||||
raise RuntimeError("no step is open")
|
||||
path = self._trial_dir / "reference.csv"
|
||||
self._reference_file.flush()
|
||||
self._reference_file.close()
|
||||
self._reference_file = None
|
||||
self._reference = None
|
||||
with path.open(encoding="utf-8-sig", newline="") as handle:
|
||||
rows = list(csv.reader(handle))
|
||||
if len(rows) < 2 or "hold_complete" not in rows[0]:
|
||||
self._reopen_reference()
|
||||
raise RuntimeError("no finished step to measure again")
|
||||
column = rows[0].index("hold_complete")
|
||||
if rows[-1][column] != "1":
|
||||
self._reopen_reference()
|
||||
raise RuntimeError("this step is already waiting to be measured again")
|
||||
rows[-1][column] = "0"
|
||||
with path.open("w", newline="", encoding="utf-8") as handle:
|
||||
csv.writer(handle).writerows(rows)
|
||||
self._reopen_reference()
|
||||
|
||||
def _reopen_reference(self) -> None:
|
||||
path = self._trial_dir / "reference.csv"
|
||||
handle = path.open("a", newline="", encoding="utf-8")
|
||||
self._reference_file = handle
|
||||
self._reference = csv.writer(handle)
|
||||
|
||||
def reacquire(self, site_id: str) -> None:
|
||||
"""Append one more repeat of a point into this same session."""
|
||||
if self.is_active:
|
||||
@@ -943,13 +1074,15 @@ class Campaign:
|
||||
return
|
||||
rows = []
|
||||
for trial in self._document.get("trials") or []:
|
||||
repeat_dir = self.directory / trial["directory"]
|
||||
rows.extend(stable_component_rows(
|
||||
self.directory / trial["directory"],
|
||||
repeat_dir,
|
||||
site_id=str(trial.get("site_id", "")),
|
||||
repeat_index=int(trial.get("repeat_index") or 0),
|
||||
uv=self._uv_of(str(trial.get("site_id", "")), repeat_dir),
|
||||
))
|
||||
path = self.directory / "stable_means.csv"
|
||||
with path.open("w", newline="", encoding="utf-8") as handle:
|
||||
with path.open("w", newline="", encoding="utf-8-sig") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=list(STABLE_MEANS_HEADER))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
@@ -963,6 +1096,22 @@ class Campaign:
|
||||
self._document["completion"] = self.completion()
|
||||
write_json(self.directory / "session.json", self._document)
|
||||
|
||||
def _uv_of(self, site_id: str, repeat_dir: Path) -> tuple[float, float] | None:
|
||||
"""UV from the repeat, then from the point still in this session."""
|
||||
u_text, v_text = _point_uv(repeat_dir)
|
||||
if u_text != "" or v_text != "":
|
||||
return float(u_text or 0), float(v_text or 0)
|
||||
for site in self.sites:
|
||||
if site.site_id == site_id and site.uv_px is not None:
|
||||
return site.uv_px
|
||||
for record in (self._document or {}).get("sites") or []:
|
||||
if str(record.get("site_id")) != site_id:
|
||||
continue
|
||||
uv = record.get("uv_px")
|
||||
if isinstance(uv, dict) and "u" in uv and "v" in uv:
|
||||
return float(uv["u"]), float(uv["v"])
|
||||
return None
|
||||
|
||||
def _steps_for(self, site: SiteSpec) -> list[tuple[str, float]]:
|
||||
return load_steps(
|
||||
list(site.levels_n), float(self.max_force_n), min_force_n=self.min_force_n,
|
||||
|
||||
@@ -191,11 +191,13 @@ class MultipointPanel(QWidget):
|
||||
self.simulate_button = QPushButton(page)
|
||||
self.start_button = QPushButton(page)
|
||||
self.liftoff_button = QPushButton(page)
|
||||
self.remeasure_button = QPushButton(page)
|
||||
self.stop_button = QPushButton(page)
|
||||
actions.addWidget(self.simulate_button, 0, 0)
|
||||
actions.addWidget(self.start_button, 0, 1)
|
||||
actions.addWidget(self.liftoff_button, 1, 0, 1, 2)
|
||||
actions.addWidget(self.stop_button, 2, 0, 1, 2)
|
||||
actions.addWidget(self.remeasure_button, 2, 0, 1, 2)
|
||||
actions.addWidget(self.stop_button, 3, 0, 1, 2)
|
||||
actions.setColumnStretch(0, 1)
|
||||
actions.setColumnStretch(1, 1)
|
||||
layout.addLayout(actions)
|
||||
@@ -213,6 +215,7 @@ class MultipointPanel(QWidget):
|
||||
self.simulate_button.clicked.connect(lambda: self._begin(simulate=True))
|
||||
self.start_button.clicked.connect(lambda: self._begin(simulate=False))
|
||||
self.liftoff_button.clicked.connect(self._liftoff)
|
||||
self.remeasure_button.clicked.connect(self._remeasure)
|
||||
self.stop_button.clicked.connect(self._stop)
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setInterval(200)
|
||||
@@ -265,6 +268,8 @@ class MultipointPanel(QWidget):
|
||||
self.simulate_button.setText(t("multipoint.simulate"))
|
||||
self.start_button.setText(t("multipoint.start"))
|
||||
self.liftoff_button.setText(t("multipoint.liftoff"))
|
||||
self.remeasure_button.setText(t("multipoint.remeasure"))
|
||||
self.remeasure_button.setToolTip(t("multipoint.remeasure_ready"))
|
||||
self.stop_button.setText(t("multipoint.stop"))
|
||||
self.table.setHorizontalHeaderLabels([
|
||||
t("multipoint.col_id"), t("multipoint.col_x"), t("multipoint.col_y"),
|
||||
@@ -537,6 +542,12 @@ class MultipointPanel(QWidget):
|
||||
def _liftoff(self) -> None:
|
||||
self._run(self.campaign.confirm_liftoff)
|
||||
|
||||
def _remeasure(self) -> None:
|
||||
def go():
|
||||
self.campaign.remeasure_step()
|
||||
self.message.setText(t("multipoint.remeasure_ready"))
|
||||
self._run(go)
|
||||
|
||||
def _stop(self) -> None:
|
||||
self.campaign.stop(t("multipoint.stopped_reason"))
|
||||
self._refresh()
|
||||
@@ -604,6 +615,7 @@ class MultipointPanel(QWidget):
|
||||
for widget in (self.fx_edit, self.fy_edit, self.fz_edit, self.rotation):
|
||||
widget.setEnabled(self.campaign.phase == "load" and wrench)
|
||||
self.liftoff_button.setEnabled(self.campaign.phase == "liftoff")
|
||||
self.remeasure_button.setEnabled(self.campaign.can_remeasure_step())
|
||||
self.stop_button.setEnabled(active)
|
||||
self.reacquire_button.setEnabled(
|
||||
not active and bool(self._selected) and self.campaign.directory is not None
|
||||
|
||||
@@ -256,13 +256,17 @@ def test_stable_means_are_the_three_components_of_the_latter_half(tmp_path):
|
||||
with (repeat / "reference.csv").open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=[
|
||||
"step_index", "branch", "target_load_n", "t_start_host_mono_ns",
|
||||
"t_end_host_mono_ns", "hold_complete",
|
||||
"t_end_host_mono_ns", "hold_complete", "reference_source",
|
||||
])
|
||||
writer.writeheader()
|
||||
writer.writerow({
|
||||
"step_index": 1, "branch": "load", "target_load_n": 5,
|
||||
"t_start_host_mono_ns": start, "t_end_host_mono_ns": end, "hold_complete": 1,
|
||||
"reference_source": "weight",
|
||||
})
|
||||
(repeat / "session.json").write_text(
|
||||
json.dumps({"uv_px": {"u": 128, "v": 64}}), encoding="utf-8",
|
||||
)
|
||||
with (repeat / "capture" / "frames.csv").open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=["frame_id", "t_host_mono_ns", "dropped"])
|
||||
writer.writeheader()
|
||||
@@ -296,6 +300,9 @@ def test_stable_means_are_the_three_components_of_the_latter_half(tmp_path):
|
||||
assert float(rows[0]["FN_mean"]) == pytest.approx(3.0)
|
||||
assert float(rows[0]["FX_mean"]) == pytest.approx(0.6)
|
||||
assert float(rows[0]["FY_mean"]) == pytest.approx(-0.4)
|
||||
assert rows[0]["U"] == "128"
|
||||
assert rows[0]["V"] == "64"
|
||||
assert rows[0]["负载类型"] == "砝码"
|
||||
assert "Fmag" not in rows[0]
|
||||
|
||||
|
||||
@@ -338,3 +345,108 @@ def test_an_outlier_in_the_stable_half_is_left_out_of_its_own_mean(tmp_path):
|
||||
assert float(row["FN_mean"]) == pytest.approx(3.0)
|
||||
assert float(row["FX_mean"]) == pytest.approx(0.575)
|
||||
assert float(row["FY_mean"]) == pytest.approx(-0.375)
|
||||
|
||||
|
||||
def test_a_reconnected_camera_keeps_each_hold_on_its_own_frames(tmp_path):
|
||||
"""A camera that starts again reuses frame numbers. Each hold keeps its own rows."""
|
||||
from orisys_autocal.multipoint import stable_component_rows
|
||||
|
||||
repeat = tmp_path / "repeat-001"
|
||||
(repeat / "capture").mkdir(parents=True)
|
||||
first = (1_000, 1_100)
|
||||
second = (5_000, 5_100)
|
||||
with (repeat / "reference.csv").open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=[
|
||||
"step_index", "branch", "target_load_n", "t_start_host_mono_ns",
|
||||
"t_end_host_mono_ns", "hold_complete", "reference_source",
|
||||
])
|
||||
writer.writeheader()
|
||||
writer.writerow({
|
||||
"step_index": 2, "branch": "load", "target_load_n": 3,
|
||||
"t_start_host_mono_ns": first[0], "t_end_host_mono_ns": first[1],
|
||||
"hold_complete": 1, "reference_source": "weight",
|
||||
})
|
||||
writer.writerow({
|
||||
"step_index": 3, "branch": "unload", "target_load_n": 2,
|
||||
"t_start_host_mono_ns": second[0], "t_end_host_mono_ns": second[1],
|
||||
"hold_complete": 1, "reference_source": "weight",
|
||||
})
|
||||
with (repeat / "capture" / "frames.csv").open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=["frame_id", "t_host_mono_ns", "dropped"])
|
||||
writer.writeheader()
|
||||
for frame_id, stamp in ((1, 1060), (2, 1080), (1, 5060), (2, 5080)):
|
||||
writer.writerow({"frame_id": frame_id, "t_host_mono_ns": stamp, "dropped": 0})
|
||||
with (repeat / "capture" / "forces.csv").open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=[
|
||||
"frame_id", "valid", "FN_output_N", "FX_output_N", "FY_output_N",
|
||||
])
|
||||
writer.writeheader()
|
||||
for frame_id, fn in ((1, 10), (2, 30), (1, 4), (2, 6)):
|
||||
writer.writerow({
|
||||
"frame_id": frame_id, "valid": 1,
|
||||
"FN_output_N": fn, "FX_output_N": 0, "FY_output_N": 0,
|
||||
})
|
||||
|
||||
rows = stable_component_rows(repeat, site_id="P1", repeat_index=1)
|
||||
|
||||
assert float(rows[0]["FN_mean"]) == pytest.approx(20.0)
|
||||
assert rows[0]["branch"] == "load"
|
||||
assert float(rows[1]["FN_mean"]) == pytest.approx(5.0)
|
||||
assert rows[1]["branch"] == "unload"
|
||||
|
||||
|
||||
def _hold_for(campaign: Campaign, force: float) -> None:
|
||||
campaign.confirm_reference(force)
|
||||
campaign.complete_hold(now_ns=campaign._hold_started_ns + int(campaign._hold_s * 1e9))
|
||||
|
||||
|
||||
def test_a_finished_step_can_be_measured_again_without_skipping_ahead(tmp_path):
|
||||
campaign = Campaign()
|
||||
campaign.add_site("A", 0, 0, 1, [2, 3], 20, 1, max_force_n=10)
|
||||
campaign.start(
|
||||
tmp_path, sensor_model="finger", sensor_sn="SN1",
|
||||
coordinate_frame="sensor", max_force_n=10, simulate=True,
|
||||
)
|
||||
_hold_for(campaign, 0)
|
||||
_hold_for(campaign, 2)
|
||||
assert campaign.progress()["target_n"] == 3
|
||||
assert campaign.can_remeasure_step()
|
||||
|
||||
campaign.remeasure_step()
|
||||
|
||||
assert campaign.phase == "load"
|
||||
assert campaign.progress()["branch"] == "load"
|
||||
assert campaign.progress()["target_n"] == 2
|
||||
assert not campaign.can_remeasure_step()
|
||||
saved = list(csv.DictReader(
|
||||
(campaign.directory / "sites" / "A" / "repeat-001" / "reference.csv").open(encoding="utf-8")
|
||||
))
|
||||
assert saved[-1]["target_load_n"] == "2.0"
|
||||
assert saved[-1]["hold_complete"] == "0"
|
||||
_hold_for(campaign, 2)
|
||||
saved = list(csv.DictReader(
|
||||
(campaign.directory / "sites" / "A" / "repeat-001" / "reference.csv").open(encoding="utf-8")
|
||||
))
|
||||
redone = [row for row in saved if row["target_load_n"] == "2.0"]
|
||||
assert [row["hold_complete"] for row in redone] == ["0", "1"]
|
||||
assert campaign.progress()["target_n"] == 3
|
||||
|
||||
|
||||
def test_a_hold_that_drops_midway_stays_on_the_same_step(tmp_path):
|
||||
campaign = Campaign()
|
||||
campaign.add_site("A", 0, 0, 1, [2, 3], 20, 1, max_force_n=10)
|
||||
campaign.start(
|
||||
tmp_path, sensor_model="finger", sensor_sn="SN1",
|
||||
coordinate_frame="sensor", max_force_n=10, simulate=True,
|
||||
)
|
||||
_hold_for(campaign, 0)
|
||||
campaign.confirm_reference(2)
|
||||
assert campaign.phase == "hold"
|
||||
campaign.remeasure_step()
|
||||
assert campaign.phase == "load"
|
||||
assert campaign.progress()["target_n"] == 2
|
||||
saved = list(csv.DictReader(
|
||||
(campaign.directory / "sites" / "A" / "repeat-001" / "reference.csv").open(encoding="utf-8")
|
||||
))
|
||||
assert saved[-1]["hold_complete"] == "0"
|
||||
assert saved[-1]["target_load_n"] == "2.0"
|
||||
|
||||
Reference in New Issue
Block a user