Skip to content

Commit e41ffe5

Browse files
ghirparaclaude
andcommitted
Close AxisRemap polish follow-up: docs, eq/hash, dead state, branch coverage
Addresses all four deferred items from daz-script-server-nfdr: - Document AxisRemap/Y_UP_TO_Z_UP in docs/api/math3.rst and README.md - Add AxisRemap.__eq__/__hash__ (Vec3 and Quat already had these) - Drop the unused _matrix/_det instance slots; kept as __init__ locals - Add a 24-permutation property test exercising all four branches of _mat3_to_quat (previously only trace>0 was covered) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 470dd67 commit e41ffe5

6 files changed

Lines changed: 85 additions & 7 deletions

File tree

.beads/interactions.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,4 @@
190190
{"id":"int-4717c43b","kind":"field_change","created_at":"2026-08-10T01:49:49.7582239Z","actor":"G.Hirpara","issue_id":"daz-script-server-x6sy","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Shipped: design spec + plan (docs/superpowers/specs|plans/2026-08-07-dazpy-hdri-environment*.md), HDRIEnvironment dataclass, apply_hdri_environment, validated environment-map setter, package exports, and readback verification (daz-script-server-jnbm) — all landed across commits 691d04c..b62ca4c"}}
191191
{"id":"int-d11b256e","kind":"field_change","created_at":"2026-08-10T02:42:16.8074426Z","actor":"G.Hirpara","issue_id":"daz-script-server-okpk","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Added AxisRemap to dazpy/math3.py: apply_vec3/apply_quat/apply_bbox with reflection rejection, Y_UP_TO_Z_UP preset, exported from dazpy package. Wired into tests.py unit runner."}}
192192
{"id":"int-27eb72b4","kind":"field_change","created_at":"2026-08-11T02:07:34.4536775Z","actor":"G.Hirpara","issue_id":"daz-script-server-063","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Won't fix, after implementation review. The already-shipped pre-check busy detection (commit 720736f, on master) already turns the original unbounded hang into a bounded, self-correcting ~15s slow-but-correct response — no data loss, no wrong result, just latency. A working bounded-wait backstop was implemented and independently verified concurrency-correct (race-free CAS ceiling, sound thread/promise lifetimes, Qt6-portable), but final review surfaced a real behavioral hazard: on timeout the queued main-thread call is NOT cancelled (Qt has no API for this) and keeps running, so the 503 the client receives is misleading (the operation is still executing), and dazpy's retry_on_busy could re-run it — a double-execution risk strictly worse than the slow-but-correct wait it replaces. The default 20s timeout also exceeds the one measured gap (~15s), so it offers little value against the observed problem and is really speculative insurance against a worse, never-observed wedged-main-thread case. Decision: the tradeoff isn't worth it as scoped; keep the existing pre-check detection only. The implementation (branch feature/studio-busy-handling, commits 680b267..467ea7b) is not merged to master. Revisit only if a genuinely unbounded hang is observed in practice, with a design that avoids the discard-and-maybe-retry hazard (e.g. never reusing a retry-safe error code for a timeout that doesn't actually stop the underlying work)."}}
193+
{"id":"int-dfa45616","kind":"field_change","created_at":"2026-08-11T02:11:53.6096817Z","actor":"G.Hirpara","issue_id":"daz-script-server-nfdr","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Docs updated (README + math3.rst), AxisRemap gained __eq__/__hash__, removed dead _matrix/_det slots (kept as locals), added 24-permutation property test covering all _mat3_to_quat branches. inverse() deliberately left as separate future item per issue note."}}

.beads/issues.jsonl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
{"_type":"issue","id":"daz-script-server-akd","title":"Document render API with VN workflow examples","description":"The render API design includes a non-obvious batch interleaving pattern that needs explicit documentation and examples to be useful for VN pipeline authors.\n\nAdd to README / dazpy docs:\n1. Basic single render example\n2. Batch variant rendering — pattern A (submit all upfront, poll)\n3. Batch variant rendering — pattern B (interleave scene setup between renders for complex changes)\n4. Multi-figure scene example (two characters, different expressions)\n5. Note on transparent backgrounds: iRay scene setup controls this; the API does not manipulate backdrop nodes\n\nEmphasize in pattern B docs: because renders are sequential in the async queue, a client can submit render N, wait for it to start, execute scene-setup scripts to configure render N+1, then submit render N+1 — achieving tight coupling between renders without the server needing to understand the full variant state.","status":"closed","priority":3,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T20:39:43Z","created_by":"G.Hirpara","updated_at":"2026-05-23T12:06:57Z","started_at":"2026-05-23T12:03:47Z","closed_at":"2026-05-23T12:06:57Z","close_reason":"Closed","dependencies":[{"issue_id":"daz-script-server-akd","depends_on_id":"daz-script-server-bm1","type":"blocks","created_at":"2026-05-21T16:40:08Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
186186
{"_type":"issue","id":"daz-script-server-h8e","title":"Add dazpy render API (render, render_variants)","description":"dazpy needs high-level Python wrappers for the render endpoints so VN pipeline scripts can trigger renders without constructing raw HTTP requests or parsing SSE streams manually.\n\nImplement in dazpy:\n DazScriptServer.render(output_path, *, figure, morphs, figures, width, height, camera, engine, wait=True, timeout=300) -\u003e RenderResult\n DazScriptServer.render_variants(variants, base=None, *, on_progress=None) -\u003e list[RenderResult]\n\nDataclasses:\n FigureMorphs(name: str|None, morphs: dict[str, float])\n RenderVariant(output_path, figure, morphs, figures, width, height)\n RenderBase(figure, width, height, camera, engine)\n RenderResult(success, output_path, file_size_bytes, duration_ms, error)\n\nrender() uses SSE progress stream when available, falls back to polling. render_variants() submits via /render/batch and tracks all request_ids, calling on_progress with (completed, total) after each render finishes.","status":"closed","priority":3,"issue_type":"feature","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T20:39:42Z","created_by":"G.Hirpara","updated_at":"2026-05-23T11:58:52Z","started_at":"2026-05-23T11:54:52Z","closed_at":"2026-05-23T11:58:52Z","close_reason":"Closed","dependencies":[{"issue_id":"daz-script-server-h8e","depends_on_id":"daz-script-server-bm1","type":"blocks","created_at":"2026-05-21T16:40:07Z","created_by":"G.Hirpara","metadata":"{}"},{"issue_id":"daz-script-server-h8e","depends_on_id":"daz-script-server-dou","type":"blocks","created_at":"2026-05-21T16:40:07Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0}
187187
{"_type":"issue","id":"daz-script-server-j25","title":"Add /scene/events to CMakeLists and API docs","description":"Two things:\n\n1. CMakeLists.txt: add src/SceneEventBroker.cpp to the source list\n\n2. README.md / docs: document the new endpoint\n - GET /scene/events section under HTTP API\n - Query params: filter=\n - Event format: {type, ts, data}\n - Full table of event types and their data fields\n - Example: curl -H 'X-API-Token: ...' http://127.0.0.1:18811/scene/events\n - Python example using sseclient or requests with stream=True\n - Note on keepalive comments (:keepalive)","status":"closed","priority":3,"issue_type":"task","owner":"ghirpara@yahoo.com","created_at":"2026-05-21T17:50:01Z","created_by":"G.Hirpara","updated_at":"2026-05-21T17:58:21Z","closed_at":"2026-05-21T17:58:21Z","close_reason":"SceneEventBroker.cpp and SceneEventBroker.h added to src/CMakeLists.txt source list","dependencies":[{"issue_id":"daz-script-server-j25","depends_on_id":"daz-script-server-2bs","type":"parent-child","created_at":"2026-05-21T13:50:16Z","created_by":"G.Hirpara","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
188-
{"_type":"issue","id":"daz-script-server-nfdr","title":"AxisRemap polish: docs, __eq__, dead state, _mat3_to_quat branch coverage","description":"Follow-up from the final review of daz-script-server-okpk (AxisRemap coordinate conversion, merged in af4bb11). Four Minor items were deferred rather than blocking merge:\n\n1. docs/api/math3.rst and README.md's math3 blurb were never updated to mention AxisRemap/Y_UP_TO_Z_UP.\n2. AxisRemap has no __eq__/__hash__ (Vec3 and Quat both do). If added, must add both together (eq without hash makes instances unhashable).\n3. AxisRemap._matrix and _det are stored on the instance but never read after construction except via the _rotation_quat gate; harmless dead state, low priority.\n4. dazpy/math3.py's _mat3_to_quat has 4 branches (trace\u003e0 plus 3 diagonal-dominant cases); the shipped test suite in tests/test_math3.py only exercises the trace\u003e0 branch. Two independent reviewers verified the other 3 branches correct out-of-band during review, but there's no regression test protecting them. A property-based test sweeping all 24 proper axis permutations (see the final review's suggested test) would close this permanently.\n\nAlso worth considering as a separate, larger follow-up (not part of this issue): an AxisRemap.inverse() method, since round-tripping currently requires hand-deriving the inverse remap by hand.","acceptance_criteria":"All four items addressed or explicitly triaged as won't-fix with reasoning.","status":"open","priority":4,"issue_type":"task","owner":"ghirpara@yahoo.com","created_at":"2026-08-10T02:49:11Z","created_by":"G.Hirpara","updated_at":"2026-08-10T02:49:11Z","dependency_count":0,"dependent_count":0,"comment_count":0}
188+
{"_type":"issue","id":"daz-script-server-nfdr","title":"AxisRemap polish: docs, __eq__, dead state, _mat3_to_quat branch coverage","description":"Follow-up from the final review of daz-script-server-okpk (AxisRemap coordinate conversion, merged in af4bb11). Four Minor items were deferred rather than blocking merge:\n\n1. docs/api/math3.rst and README.md's math3 blurb were never updated to mention AxisRemap/Y_UP_TO_Z_UP.\n2. AxisRemap has no __eq__/__hash__ (Vec3 and Quat both do). If added, must add both together (eq without hash makes instances unhashable).\n3. AxisRemap._matrix and _det are stored on the instance but never read after construction except via the _rotation_quat gate; harmless dead state, low priority.\n4. dazpy/math3.py's _mat3_to_quat has 4 branches (trace\u003e0 plus 3 diagonal-dominant cases); the shipped test suite in tests/test_math3.py only exercises the trace\u003e0 branch. Two independent reviewers verified the other 3 branches correct out-of-band during review, but there's no regression test protecting them. A property-based test sweeping all 24 proper axis permutations (see the final review's suggested test) would close this permanently.\n\nAlso worth considering as a separate, larger follow-up (not part of this issue): an AxisRemap.inverse() method, since round-tripping currently requires hand-deriving the inverse remap by hand.","acceptance_criteria":"All four items addressed or explicitly triaged as won't-fix with reasoning.","status":"closed","priority":4,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-08-10T02:49:11Z","created_by":"G.Hirpara","updated_at":"2026-08-11T02:11:54Z","started_at":"2026-08-11T02:10:29Z","closed_at":"2026-08-11T02:11:54Z","close_reason":"Docs updated (README + math3.rst), AxisRemap gained __eq__/__hash__, removed dead _matrix/_det slots (kept as locals), added 24-permutation property test covering all _mat3_to_quat branches. inverse() deliberately left as separate future item per issue note.","dependency_count":0,"dependent_count":0,"comment_count":0}
189189
{"_type":"issue","id":"daz-script-server-0v4","title":"USD export: consider USDC binary output to resolve ASCII performance warning","description":"nvidia_usd_validate UsdAsciiPerformanceChecker reports 1 failure — the file is exported as ASCII .usda which is slow to parse for large assets. Fix: add an option in daz-bridge-usd to export as USDC (binary crate format) or USDZ (zip). ASCII is fine for debugging but binary should be the default for production exports.","status":"closed","priority":4,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-23T19:12:33Z","created_by":"G.Hirpara","updated_at":"2026-05-24T03:00:31Z","started_at":"2026-05-24T02:52:31Z","closed_at":"2026-05-24T03:00:31Z","close_reason":"Added USD Binary (*.usdc) as default filter in file dialog; writeConfiguration() routes .usdc output through snapshot+writeUsdcFromSnapshot path instead of UsaWriter","dependency_count":0,"dependent_count":0,"comment_count":0}
190190
{"_type":"issue","id":"daz-script-server-00i","title":"USD export: weld co-located points in eyebrow card mesh","description":"nvidia_usd_validate WeldChecker reports 1 warning — /Root/Genesis9/G9EyebrowCards/points contains co-located vertices that could be merged. This inflates vertex count without adding geometric detail. Fix: in daz-bridge-usd, run a weld/merge pass on the eyebrow card mesh vertices before authoring the UsdGeomMesh, or apply a general weld step to all exported meshes.","status":"closed","priority":4,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-23T19:12:30Z","created_by":"G.Hirpara","updated_at":"2026-05-24T03:00:30Z","started_at":"2026-05-24T02:52:31Z","closed_at":"2026-05-24T03:00:30Z","close_reason":"Weld co-located vertices in both USDA (exportMeshGeometry) and USDC (writeMesh) paths using exact-match position map; face vertex indices remapped through weld table","dependency_count":0,"dependent_count":0,"comment_count":0}
191191
{"_type":"issue","id":"daz-script-server-0g3","title":"USD export: index repeated primvars:st values on eyebrow cards","description":"nvidia_usd_validate IndexedPrimvarChecker reports 1 warning — primvars:st on /Root/Genesis9/G9EyebrowCards contains repeated values that could be indexed, reducing file size. Fix: in daz-bridge-usd, deduplicate UV values and author primvars:st with a indices array (indexed primvar format) rather than expanded per-vertex values.","status":"closed","priority":4,"issue_type":"task","assignee":"G.Hirpara","owner":"ghirpara@yahoo.com","created_at":"2026-05-23T19:12:27Z","created_by":"G.Hirpara","updated_at":"2026-05-24T03:00:31Z","started_at":"2026-05-24T02:52:31Z","closed_at":"2026-05-24T03:00:31Z","close_reason":"Deduplicate primvars:st UV values in both paths using exact-match map; uvIndices remapped through dedup table before authoring","dependency_count":0,"dependent_count":0,"comment_count":0}

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,11 +606,14 @@ print(anim.frame_range()) # (start, end)
606606
anim.bake_pose_to_keyframes(start=0, end=60)
607607
```
608608

609-
### 📐 math3 — Vec3, Quat, BoundingBox
609+
### 📐 math3 — Vec3, Quat, BoundingBox, AxisRemap
610610

611611
New module **`dazpy.math3`** provides lightweight value types returned throughout
612612
the SDK: `Vec3` (positions, translations), `Quat` (rotations), and `BoundingBox`
613613
(geometry bounds). All support arithmetic operators and round-trip through JSON.
614+
`AxisRemap` converts `Vec3`/`Quat`/`BoundingBox` values between axis conventions
615+
via signed-axis permutations (e.g. the built-in `Y_UP_TO_Z_UP` preset for
616+
converting DAZ Studio's Y-up scene data to Z-up tools like Blender).
614617

615618
### 🌐 Posed Vertex Export & USD Scene Export
616619

@@ -656,7 +659,7 @@ Studio instance) now ship in the repo root.
656659

657660
### 📖 Docs & Ergonomics
658661

659-
- API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, and `BoundingBox`
662+
- API reference pages for `DazPose`, `DazAnimation`, `Vec3`, `Quat`, `BoundingBox`, and `AxisRemap`
660663
- Every script in `docs/examples/` has an `if __name__ == "__main__":` guard and
661664
argparse `--help`, making all examples safe to import and self-documenting
662665

dazpy/math3.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ class AxisRemap:
697697
z: Source axis for the output Z component.
698698
"""
699699

700-
__slots__ = ("_specs", "_matrix", "_det", "_rotation_quat")
700+
__slots__ = ("_specs", "_rotation_quat")
701701

702702
def __init__(self, x: str, y: str, z: str) -> None:
703703
specs = (_parse_axis_spec(x), _parse_axis_spec(y), _parse_axis_spec(z))
@@ -710,10 +710,8 @@ def __init__(self, x: str, y: str, z: str) -> None:
710710
matrix = [[0.0, 0.0, 0.0] for _ in range(3)]
711711
for row, (idx, sign) in enumerate(specs):
712712
matrix[row][idx] = sign
713-
object.__setattr__(self, "_specs", specs)
714-
object.__setattr__(self, "_matrix", matrix)
715713
det = _mat3_det(matrix)
716-
object.__setattr__(self, "_det", det)
714+
object.__setattr__(self, "_specs", specs)
717715
object.__setattr__(
718716
self, "_rotation_quat", _mat3_to_quat(matrix) if det > 0 else None
719717
)
@@ -728,6 +726,14 @@ def __repr__(self) -> str:
728726
parts.append(("-" if sign < 0 else "") + axes[idx])
729727
return f"AxisRemap(x={parts[0]!r}, y={parts[1]!r}, z={parts[2]!r})"
730728

729+
def __eq__(self, other: object) -> bool:
730+
if not isinstance(other, AxisRemap):
731+
return NotImplemented
732+
return self._specs == other._specs
733+
734+
def __hash__(self) -> int:
735+
return hash(self._specs)
736+
731737
# ── application ───────────────────────────────────────────────────────────
732738

733739
def apply_vec3(self, v: "Vec3") -> "Vec3":

docs/api/math3.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,13 @@ BoundingBox
2424
:members:
2525
:undoc-members:
2626
:show-inheritance:
27+
28+
AxisRemap
29+
---------
30+
31+
.. autoclass:: dazpy.AxisRemap
32+
:members:
33+
:undoc-members:
34+
:show-inheritance:
35+
36+
.. autodata:: dazpy.Y_UP_TO_Z_UP

tests/test_math3.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Via runner: python tests.py unit
88
"""
99

10+
import itertools
1011
import os
1112
import sys
1213
import unittest
@@ -109,6 +110,63 @@ def test_sign_flip_keeps_min_max_correctly_ordered(self):
109110
self.assertEqual(result.max, Vec3(1, 0, 2))
110111

111112

113+
class TestAxisRemapEquality(unittest.TestCase):
114+
def test_equal_specs_are_equal_and_hash_equal(self):
115+
a = AxisRemap(x="x", y="-z", z="y")
116+
b = AxisRemap(x="x", y="-z", z="y")
117+
self.assertEqual(a, b)
118+
self.assertEqual(hash(a), hash(b))
119+
120+
def test_different_specs_are_unequal(self):
121+
a = AxisRemap(x="x", y="y", z="z")
122+
b = AxisRemap(x="x", y="-z", z="y")
123+
self.assertNotEqual(a, b)
124+
125+
def test_eq_against_other_type_is_not_implemented(self):
126+
remap = AxisRemap(x="x", y="y", z="z")
127+
self.assertNotEqual(remap, "not a remap")
128+
129+
130+
class TestMat3ToQuatBranchCoverage(unittest.TestCase):
131+
"""Sweeps all 24 proper (det == +1) signed axis permutations so every
132+
branch of dazpy.math3._mat3_to_quat (trace>0 and the three
133+
diagonal-dominant cases) gets exercised, not just trace>0.
134+
"""
135+
136+
AXES = ("x", "y", "z")
137+
_PROBE_VECTORS = (Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0, 0, 1), Vec3(1, 2, 3))
138+
139+
@classmethod
140+
def _proper_remaps(cls):
141+
remaps = []
142+
for perm in itertools.permutations(cls.AXES):
143+
for signs in itertools.product((1, -1), repeat=3):
144+
specs = [("-" if s < 0 else "") + axis for axis, s in zip(perm, signs)]
145+
remap = AxisRemap(x=specs[0], y=specs[1], z=specs[2])
146+
if remap._rotation_quat is not None:
147+
remaps.append(remap)
148+
return remaps
149+
150+
def test_exactly_24_proper_permutations_exist(self):
151+
# 3! axis orderings * 2^3 sign choices = 48 total; exactly half
152+
# (24) have determinant +1 (proper rotations).
153+
self.assertEqual(len(self._proper_remaps()), 24)
154+
155+
def test_rotation_quat_matches_matrix_for_all_proper_permutations(self):
156+
for remap in self._proper_remaps():
157+
q = remap._rotation_quat
158+
for v in self._PROBE_VECTORS:
159+
expected = remap.apply_vec3(v)
160+
actual = q.rotate(v)
161+
self.assertAlmostEqual(actual.x, expected.x, places=9, msg=repr(remap))
162+
self.assertAlmostEqual(actual.y, expected.y, places=9, msg=repr(remap))
163+
self.assertAlmostEqual(actual.z, expected.z, places=9, msg=repr(remap))
164+
165+
def test_rotation_quat_is_unit_length_for_all_proper_permutations(self):
166+
for remap in self._proper_remaps():
167+
self.assertAlmostEqual(remap._rotation_quat.length(), 1.0, places=9, msg=repr(remap))
168+
169+
112170
class TestYUpToZUpPreset(unittest.TestCase):
113171
def test_preset_matches_manual_equivalent(self):
114172
manual = AxisRemap(x="x", y="-z", z="y")

0 commit comments

Comments
 (0)