Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dotbot/calibration/conics.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,8 @@ def field_angle(centres: np.ndarray) -> float:

# What a free-mode calibration records as its site's anchor: no mark in the
# room is its origin.
# A site made by `site init` records the calibration it came from in its anchor
SELF_DEFINED_ANCHOR = "self-defined from spin calibration "
FREE_FRAME_ANCHOR = (
"free mode: zero at the top-left of the rectangle around the spinning "
"robots, grown by a robot's footprint; axes along that rectangle"
Expand Down Expand Up @@ -822,7 +824,7 @@ def self_defined_site(
extent, field_area, staging = starter_layout((field_w, field_h), size_mm)
dx, dy = field_area.x, field_area.y
shift = np.array([[1, 0, dx], [0, 1, dy], [0, 0, 1.0]])
anchor = f"self-defined from spin calibration {calibration.id8}"
anchor = f"{SELF_DEFINED_ANCHOR}{calibration.id8}"
stations = []
for st in calibration.stations:
H = shift @ st.matrix
Expand Down Expand Up @@ -853,5 +855,6 @@ def self_defined_site(
created_at=calibration.created_at,
tag=f"{calibration.tag}-{name}" if calibration.tag else "",
robot=calibration.robot,
placed_from=calibration.id,
)
return site, placed
14 changes: 14 additions & 0 deletions dotbot/calibration/lighthouse2.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ class Calibration:
created_at: str = ""
tag: str = ""
robot: str = ROBOT_DEFAULT
# The id of the free-frame spin calibration this one was moved from
placed_from: str = ""
path: Optional[Path] = None
# The id the file on disk declares, when this came from one. A mismatch
# with `id` means the file was hand-edited into a different calibration.
Expand Down Expand Up @@ -686,6 +688,8 @@ def render_calibration(calibration: Calibration) -> str:
]
if calibration.tag:
out.append(f'tag = "{toml_escape(calibration.tag)}"')
if calibration.placed_from:
out.append(f'placed_from = "{calibration.placed_from}"')
out += [
"",
"[site]",
Expand Down Expand Up @@ -867,6 +871,7 @@ def read_calibration_file(path: Path) -> Calibration:
created_at=metadata.get("created_at", ""),
tag=metadata.get("tag", ""),
robot=metadata.get("robot", ROBOT_DEFAULT),
placed_from=str(metadata.get("placed_from", "")),
path=path,
)
calibration.stored_id = str(metadata.get("id", ""))
Expand Down Expand Up @@ -1043,6 +1048,15 @@ def _file_metadata(path: Path) -> dict:
return {}


def calibration_site_of(spec: str) -> Optional[str]:
"""The site of the one calibration file under any site that `spec`
names, None when none or several do."""
try:
return read_calibration_file(resolve_calibration_path(spec)).site.name
except (OSError, ValueError):
return None


def load_calibration(
spec: str,
root: Optional[Path] = None,
Expand Down
24 changes: 20 additions & 4 deletions dotbot/calibration/multi_station.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,17 +1174,33 @@ def appendable_rounds(calibration: Calibration) -> int:
calibration is in a frame a re-solve would not keep.
"""
# pylint: disable=import-outside-toplevel
from dotbot.calibration.conics import FREE_FRAME_ANCHOR
from dotbot.calibration.conics import FREE_FRAME_ANCHOR, SELF_DEFINED_ANCHOR
from dotbot.calibration.lighthouse2 import calibration_site_of

if (
not calibration.tracks
or calibration.site.anchor != FREE_FRAME_ANCHOR
or not all(st.solved_from.startswith("conics") for st in calibration.stations)
):
raise ValueError(
refused = (
f"calibration {calibration.id8} is not a free-mode spin calibration, "
"so a round cannot be appended to it; append to the spin calibration "
"it was made from"
"so a round cannot be appended to it"
)
anchor = calibration.site.anchor
if calibration.placed_from:
source = calibration.placed_from
how = f"it was placed from spin calibration {source[:8]}"
elif anchor.startswith(SELF_DEFINED_ANCHOR):
source = anchor[len(SELF_DEFINED_ANCHOR) :].strip()
how = f"its site was defined from spin calibration {source[:8]}"
else:
raise ValueError(
f"{refused}; append to the spin calibration it was made from"
)
where = calibration_site_of(source)
raise ValueError(
f"{refused}: {how}, so append to that one"
+ (f" (--append {source[:8]} --site {where})" if where else "")
)
return max(t.round for t in calibration.tracks) + 1

Expand Down
41 changes: 41 additions & 0 deletions dotbot/tests/test_calibration_spin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from dotbot.calibration import conics, lighthouse2, push
from dotbot.calibration import spin as spin_module
from dotbot.calibration.lighthouse2 import counts_for_camera_point, load_calibration
from dotbot.calibration.multi_station import appendable_rounds
from dotbot.calibration.spin import (
SPIN_CHUNK_RECORDS,
SPIN_TAG,
Expand Down Expand Up @@ -746,6 +747,46 @@ def test_append_refuses_a_corner_calibration(monkeypatch, lab, tmp_path):
assert fleet.calls == []


def test_append_to_a_placed_calibration_names_the_one_it_came_from(monkeypatch, lab):
_, fleet = _fleet()
result = _collect(monkeypatch, lab, fleet)
free_id = re.search(r"--from-calibration (\w+)", result.output).group(1)
result = CliRunner().invoke(
site_cmd.cmd,
["init", "spun", "--from-calibration", free_id],
obj={"config": lab},
)
placed_id = re.search(r"push (\w+) --site spun", result.output).group(1)
_, fleet = _fleet()
result = _collect(monkeypatch, lab, fleet, "--site", "spun", "--append", placed_id)
assert result.exit_code != 0
assert "not a free-mode spin calibration" in result.output
assert f"placed from spin calibration {free_id}" in result.output
assert f"--append {free_id} --site c405" in result.output
assert fleet.calls == []


def test_a_placed_file_without_its_source_falls_back_to_the_site_anchor(
monkeypatch, lab
):
_, fleet = _fleet()
result = _collect(monkeypatch, lab, fleet)
free_id = re.search(r"--from-calibration (\w+)", result.output).group(1)
result = CliRunner().invoke(
site_cmd.cmd,
["init", "spun", "--from-calibration", free_id],
obj={"config": lab},
)
path = Path(re.search(r"saved to (\S+)", result.output).group(1))
text = path.read_text()
assert f'placed_from = "{free_id}' in text
path.write_text(re.sub(r'placed_from = "\w+"\n', "", text))
with pytest.raises(ValueError) as refused:
appendable_rounds(lighthouse2.read_calibration_file(path))
assert f"its site was defined from spin calibration {free_id}" in str(refused.value)
assert f"--append {free_id} --site c405" in str(refused.value)


def test_append_and_drop_station_go_with_spin(lab):
for flags in (["--append", "x"], ["--drop-station", "2"]):
result = CliRunner().invoke(
Expand Down
Loading