Fastfill-test / code /fastfill /v2 /schema.py
XXXpilar's picture
FastFill v4 base (step 6552): LoRA adapter, geometry heads, tokenizer, training code, predict.py
e6f80f3 verified
Raw History Blame Contribute Delete
17.3 kB
"""Strict canonical protocol. Validation never repairs or mutates inputs."""
from __future__ import annotations
import math
from collections.abc import Mapping, Sequence
from fastfill.v2 import SCHEMA_VERSION
def _keys(value, allowed, required, label):
if not isinstance(value, Mapping):
raise ValueError(f"{label} must be an object")
if set(value) - set(allowed):
raise ValueError(f"{label}: unknown fields {sorted(set(value) - set(allowed))}")
if set(required) - set(value):
raise ValueError(f"{label}: missing fields {sorted(set(required) - set(value))}")
def finite(value, label="value", positive=False):
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise ValueError(f"{label} must be a finite number")
if positive and value <= 0:
raise ValueError(f"{label} must be strictly positive")
return float(value)
def vector(value, n=3, label="vector", positive=False):
if not isinstance(value, (list, tuple)) or len(value) != n:
raise ValueError(f"{label} must have length {n}")
return [finite(v, label, positive) for v in value]
def _id(value, label="id"):
if not isinstance(value, str) or not value.strip() or len(value) > 256:
raise ValueError(f"{label} must be a nonempty string of at most 256 characters")
return value
def _polygon(value, label):
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)) or len(value) < 3:
raise ValueError(f"{label} needs at least three XY points")
points = [vector(p, 2, label) for p in value]
area = abs(sum(a[0] * b[1] - b[0] * a[1]
for a, b in zip(points, points[1:] + points[:1]))) / 2
if area <= 0:
raise ValueError(f"{label} must have positive area")
# Exact shape validity is checked once at the input boundary, not repaired by buffer(0).
from shapely.geometry import Polygon
if not Polygon(points).is_valid:
raise ValueError(f"{label} is self-intersecting or otherwise invalid")
ROOM_FIELDS = {"frame", "floor_polygon_xy_m", "floor_z_m", "height_m", "boundary_quality",
"boundary_known", "floor_known", "fixed_objects", "room_type", "openings", "description"}
# exchangeable_group is supervision bookkeeping: it lives in validity (target order), never in the condition.
OBJECT_FIELDS = {"id", "category", "description", "support_parent", "support_surface_id",
"size_bounds_local_m", "fixed_size_local_m", "required_capabilities",
"retrieval_tolerance", "retrieval_tolerance_log", "attributes",
"constraint_role", "semantic_front_required"}
def migrate_legacy_row(row):
"""Move pre-C1 ``condition.objects[i].exchangeable_group`` into ``validity.exchangeable_group``
and give pre-K1 rows an all-false ``validity.size_axis_swap_allowed``.
The returned row never renders groups into the condition. Labels follow
target order (like validity.position); rows without targets keep request order.
"""
objects = row.get("condition", {}).get("objects", [])
if not isinstance(objects, list):
return row
order = row.get("target", {}).get("objects") or objects
if any(isinstance(o, Mapping) and "exchangeable_group" in o for o in objects):
validity = row.get("validity", {})
if "exchangeable_group" in validity:
raise ValueError("exchangeable_group declared in both condition objects and validity")
groups = {o.get("id"): o.get("exchangeable_group") for o in objects}
condition = {**row["condition"], "objects": [{k: v for k, v in o.items() if k != "exchangeable_group"} for o in objects]}
row = {**row, "condition": condition,
"validity": {**validity, "exchangeable_group": [groups.get(o.get("id")) for o in order]}}
validity = row.get("validity")
if isinstance(validity, Mapping) and "size_axis_swap_allowed" not in validity:
row = {**row, "validity": {**validity, "size_axis_swap_allowed": [False] * len(order)}}
return row
def _metadata(value, label, depth=0):
"""Allow JSON metadata, with explicit nulls and finite numeric values."""
if depth > 32:
raise ValueError(f"{label} metadata exceeds nesting limit")
if value is None or isinstance(value, (str, bool)):
return
if isinstance(value, (int, float)):
finite(value, label)
elif isinstance(value, Mapping):
if any(not isinstance(k, str) for k in value):
raise ValueError(f"{label} metadata keys must be strings")
for child in value.values():
_metadata(child, label, depth + 1)
elif isinstance(value, (list, tuple)):
for child in value:
_metadata(child, label, depth + 1)
else:
raise ValueError(f"{label} must contain JSON-compatible metadata")
def _capabilities(value, label, unknown_allowed=False):
if unknown_allowed and value is None:
return
if not isinstance(value, (list, tuple)) or any(not isinstance(v, str) or not v.strip() for v in value):
raise ValueError(f"{label} must contain nonempty capability names")
def _surfaces(value):
if value is None:
return
if not isinstance(value, (list, tuple)):
raise ValueError("verified support surfaces must be a list or null")
ids = []
for surface in value:
_keys(surface, {"surface_id", "local_polygon_xy_m", "local_z_m"},
{"surface_id", "local_polygon_xy_m", "local_z_m"}, "support surface")
ids.append(_id(surface["surface_id"], "surface id"))
_polygon(surface["local_polygon_xy_m"], "support surface polygon")
finite(surface["local_z_m"], "support height")
if len(ids) != len(set(ids)):
raise ValueError("support surface IDs must be unique per asset")
def _support_graph(objects):
parents = {obj["id"]: obj.get("support_parent") for obj in objects}
for obj in objects:
seen, current = set(), obj["id"]
while current in parents:
if current in seen:
raise ValueError("support graph contains a cycle")
seen = seen | {current}
current = parents[current]
def _object(obj):
_keys(obj, OBJECT_FIELDS, {"id", "category", "description"}, "request object")
_id(obj["id"])
_id(obj["category"], "category")
if not isinstance(obj["description"], str):
raise ValueError("description must be a string")
for key in ("support_parent", "support_surface_id"):
if key in obj and obj[key] is not None:
_id(obj[key], key)
if "constraint_role" in obj:
_id(obj["constraint_role"], "constraint role")
if "semantic_front_required" in obj and not isinstance(obj["semantic_front_required"], bool):
raise ValueError("semantic_front_required must be a boolean")
if "attributes" in obj:
if not isinstance(obj["attributes"], Mapping):
raise ValueError("attributes must be an object")
_metadata(obj["attributes"], "attributes")
if "fixed_size_local_m" in obj:
fixed = obj["fixed_size_local_m"]
if not isinstance(fixed, (list, tuple)) or len(fixed) != 3:
raise ValueError("fixed size must contain three numbers or nulls")
for value in fixed:
if value is not None:
finite(value, "fixed size", positive=True)
if "size_bounds_local_m" in obj:
bounds = obj["size_bounds_local_m"]
_keys(bounds, {"min", "max"}, {"min", "max"}, "size bounds")
lo, hi = vector(bounds["min"], positive=True), vector(bounds["max"], positive=True)
if any(a > b for a, b in zip(lo, hi)):
raise ValueError("size bounds min exceeds max")
if "fixed_size_local_m" in obj:
if any(v is not None and not a <= v <= b for v, a, b in zip(obj["fixed_size_local_m"], lo, hi)):
raise ValueError("fixed size conflicts with size bounds")
if "required_capabilities" in obj:
_capabilities(obj["required_capabilities"], "required_capabilities")
for key in ("retrieval_tolerance", "retrieval_tolerance_log"):
if key in obj:
value = obj[key]
values = vector(value) if isinstance(value, (list, tuple)) else [finite(value)]
if any(v < 0 for v in values):
raise ValueError("retrieval tolerance must be nonnegative")
def _constraints(constraints, refs):
if not isinstance(constraints, list):
raise ValueError("constraints must be a list")
for c in constraints:
if not isinstance(c, Mapping) or not isinstance(c.get("type"), str):
raise ValueError("constraint needs a type")
kind = _id(c["type"], "constraint type")
_metadata(c, "constraint")
if "hard" in c and not isinstance(c["hard"], bool):
raise ValueError("constraint hard flag must be a boolean")
if kind in {"faces_direction", "faces", "near", "clearance", "on", "between", "against_wall"}:
if c.get("object_id") not in refs:
raise ValueError("known constraint requires an object reference")
if kind in {"faces", "near", "clearance"}:
if c.get("target_id") not in refs or c["target_id"] == c["object_id"]:
raise ValueError("binary constraint requires a distinct object target")
if kind == "between":
targets = c.get("target_ids")
if (not isinstance(targets, list) or len(targets) != 2
or any(not isinstance(target, str) for target in targets) or len(set(targets)) != 2 or any(
target not in refs or target == c["object_id"] for target in targets)):
raise ValueError("between requires two distinct valid target objects")
if kind == "on":
target = c.get("target_id", c.get("parent_id"))
if target not in refs | {"floor", "wall"} or target == c["object_id"]:
raise ValueError("on constraint requires a distinct support parent")
if "target_id" in c and "parent_id" in c and c["target_id"] != c["parent_id"]:
raise ValueError("on constraint has conflicting parent references")
for kind_name, field in (("near", "max_distance_m"), ("clearance", "min_distance_m")):
if kind == kind_name and field not in c:
raise ValueError(f"{kind} constraint requires {field}")
for key in ("object_id", "target_id", "parent_id"):
if key in c and c[key] not in refs | {"floor", "wall"}:
raise ValueError(f"constraint references missing {key}")
if c["type"] == "faces_direction":
direction = vector(c.get("direction_xy"), 2)
if math.hypot(*direction) == 0:
raise ValueError("facing direction must be nonzero")
if c["type"] == "keepout":
_polygon(c.get("polygon_xy_m"), "keepout polygon")
for key in ("tolerance_rad", "tolerance_m", "max_distance_m", "min_distance_m"):
if key in c and finite(c[key], key) < 0:
raise ValueError(f"{key} must be nonnegative")
def validate_condition(condition):
_keys(condition, {"schema_version", "room", "objects", "constraints"},
{"schema_version", "room", "objects", "constraints"}, "condition")
if condition["schema_version"] != SCHEMA_VERSION:
raise ValueError("unsupported schema version")
room = condition["room"]
_keys(room, ROOM_FIELDS, {"frame", "floor_polygon_xy_m"}, "room")
if room["frame"] != "right_handed_z_up":
raise ValueError("room frame must be right_handed_z_up")
_polygon(room["floor_polygon_xy_m"], "floor polygon")
for key in ("floor_z_m", "height_m"):
if room.get(key) is not None:
finite(room[key], key, positive=key == "height_m")
for key in ("floor_known", "boundary_known"):
if key in room and not isinstance(room[key], bool):
raise ValueError(f"{key} must be a boolean")
if room.get("floor_known") is True and room.get("floor_z_m") is None:
raise ValueError("known floor requires a floor height")
fixed = room.get("fixed_objects", [])
if not isinstance(fixed, list):
raise ValueError("fixed objects must be a list")
fixed_ids = []
for obj in fixed:
_keys(obj, {"id", "size_local_m", "bottom_center_m", "yaw_rad", "category", "support_surfaces",
"capabilities", "required_capabilities", "semantic_front_local"},
{"id", "size_local_m", "bottom_center_m", "yaw_rad"}, "fixed object")
fixed_ids.append(_id(obj["id"]))
vector(obj["size_local_m"], positive=True)
vector(obj["bottom_center_m"])
finite(obj["yaw_rad"], "fixed yaw")
if "category" in obj:
_id(obj["category"], "fixed category")
for key in ("capabilities", "required_capabilities"):
if key in obj:
_capabilities(obj[key], key, unknown_allowed=key == "capabilities")
if "support_surfaces" in obj:
_surfaces(obj["support_surfaces"])
if obj.get("semantic_front_local") is not None:
front = vector(obj["semantic_front_local"])
if math.hypot(*front[:2]) == 0 or abs(front[2]) > 1e-6:
raise ValueError("semantic front must be a nonzero horizontal vector")
if not isinstance(condition["objects"], list):
raise ValueError("objects must be a list")
for obj in condition["objects"]:
_object(obj)
ids = [obj["id"] for obj in condition["objects"]] + fixed_ids
if len(set(ids)) != len(ids) or any(i in {"floor", "wall"} for i in ids):
raise ValueError("object IDs must be unique and not reserved")
for obj in condition["objects"]:
parent = obj.get("support_parent")
if parent is not None and (parent not in set(ids) | {"floor", "wall"} or parent == obj["id"]):
raise ValueError("invalid support parent")
_support_graph(condition["objects"])
for key in ("room_type", "description", "boundary_quality"):
if room.get(key) is not None and not isinstance(room[key], str):
raise ValueError(f"room {key} must be a string or null")
if "openings" in room:
if not isinstance(room["openings"], list):
raise ValueError("room openings must be a list")
_metadata(room["openings"], "room openings")
_constraints(condition["constraints"], set(ids))
from fastfill.v2.validation import effective_support_requests
effective_support_requests(condition)
return condition
def validate_layout(layout, condition=None):
_keys(layout, {"schema_version", "objects"}, {"schema_version", "objects"}, "layout")
if layout["schema_version"] != SCHEMA_VERSION or not isinstance(layout["objects"], list):
raise ValueError("invalid layout schema")
ids = []
for obj in layout["objects"]:
_keys(obj, {"id", "target_size_local_m", "bottom_center_m", "yaw_rad", "support_parent"},
{"id", "target_size_local_m", "bottom_center_m", "yaw_rad"}, "output object")
if "support_parent" in obj:
_id(obj["support_parent"], "output support_parent")
ids.append(_id(obj["id"]))
vector(obj["target_size_local_m"], positive=True)
vector(obj["bottom_center_m"])
yaw = finite(obj["yaw_rad"], "yaw")
if not -math.pi <= yaw < math.pi:
raise ValueError("yaw must be wrapped to [-pi, pi)")
if len(ids) != len(set(ids)):
raise ValueError("duplicate output IDs")
if condition is not None and set(ids) != {o["id"] for o in condition["objects"]}:
raise ValueError("output IDs must equal all requested IDs exactly once")
# optional predicted support: floor, wall, another output object or a fixed object; never itself, no cycle
parents = {obj["id"]: obj["support_parent"] for obj in layout["objects"] if "support_parent" in obj}
known = {"floor", "wall", *ids, *(f["id"] for f in (condition or {}).get("room", {}).get("fixed_objects", ()))}
for child, parent in parents.items():
if parent == child or (condition is not None and parent not in known):
raise ValueError(f"output support_parent of {child!r} must name the floor, a wall, another object or a fixed object")
seen = {child}
while parent in parents:
if parent in seen:
raise ValueError("output support_parent references form a cycle")
seen.add(parent)
parent = parents[parent]
return layout
def normalize_room(room, reference_height=3.0):
"""Origin/scales depend only on room input; missing height uses fixed 3m."""
points = room["floor_polygon_xy_m"]
xmin, ymin = [min(p[q] for p in points) for q in range(2)]
xmax, ymax = [max(p[q] for p in points) for q in range(2)]
height = room.get("height_m") or finite(reference_height, positive=True)
if xmax <= xmin or ymax <= ymin:
raise ValueError("room XY normalization needs positive extents")
return [float(xmin), float(ymin), float(room.get("floor_z_m") or 0.)], [float(xmax - xmin), float(ymax - ymin), float(height)]