--- jupyter: description: 'The symmetric difference in planex2d, a ^ b and px.SymmetricDifference: what two revisions of a plate add and remove, the crescents of an eccentric bore, and a checkerboard from eight bars, each checked against its area formula.' --- # Symmetric difference `a ^ b` keeps what lies in exactly one of two faces, the union less the intersection, and `px.SymmetricDifference` does the same for a list of parts, keeping what an odd number of them covers. The result is the region where the parts disagree, which makes it the natural comparison of two versions of a part, or of a part and its nominal geometry. Each face of the result keeps the name and the color of the part it comes from, so the material that one part adds and the material it removes can be told apart. ```python import math import planex as px ``` ```python nbsphinx="hidden" from docs_figures import CYAN, RED, Row ``` ## Two revisions of a part The example is a mounting plate in two revisions. Revision A is 80 × 50 with two holes of diameter 8, 50 apart. Revision B is 10 longer, and its second hole has moved with the right edge, to 60 from the first (Figure 1a). The symmetric difference holds three faces. Revision B adds two of them, the strip of 10 × 50 and the disc where the old hole was, and it removes the third, the disc of its new hole, from the material of revision A (Figure 1b). ```python old = px.QuickSketch().Rectangle(80, 50).Circle((15, 25), 4).Circle((65, 25), 4).Face() new = px.QuickSketch().Rectangle(90, 50).Circle((15, 25), 4).Circle((75, 25), 4).Face() old.name, new.name = "revision A", "revision B" change = old ^ new print([f.name for f in change.faces]) ``` ```python nbsphinx="hide-input" added_faces, removed_faces = list(change.faces(name="revision B")), list(change.faces(name="revision A")) view = ((-4, 94), (-4, 54)) Row((new, "Revision B, with revision A dashed", {"fill": False, "dashed": list(old.wires), "view": view}), (change, "Added in cyan, removed in red", {"fills": [(f, CYAN) for f in added_faces] + [(f, RED) for f in removed_faces], "dashed": list(new.wires), "view": view}), caption="Two revisions of a plate and their symmetric difference") ``` Selected by name, the faces give the material added and the material removed. The first is the strip and one hole, the second one hole: ```python added = sum(f.mass for f in change.faces(name="revision B")) removed = sum(f.mass for f in change.faces(name="revision A")) print("added", added, "exact", 10 * 50 + math.pi * 4**2) print("removed", removed, "exact", math.pi * 4**2) ``` The symmetric difference is the same region as `(a - b) + (b - a)`, made in one operation, and its area is the area of both parts less twice the area they share: ```python print("area", change.mass, "exact", old.mass + new.mass - 2 * (old * new).mass) ``` ## An eccentric bore The area of the symmetric difference measures how far a part is from its nominal geometry. The example is a bore of diameter 20 drilled 0.5 off its position. As discs, the nominal and the drilled bore differ in two crescents, at most 0.5 wide. The crescent on the right is material that the drill cut away outside the nominal bore, and the one on the left is material that it left standing inside the nominal bore (Figure 2). Two circles of radius $r$ whose centers are $e$ apart share a lens of area $2 r^2 \arccos\left(\tfrac{e}{2r}\right) - \tfrac{e}{2} \sqrt{4r^2 - e^2}$, so the crescents have the area of both discs less twice the lens. For an offset small against the radius, that is close to $4 r e$. ```python r = 10 e = 0.5 nominal = px.QuickSketch().Circle((0, 0), r).Face() drilled = px.QuickSketch().Circle((e, 0), r).Face() crescents = nominal ^ drilled lens = 2 * r**2 * math.acos(e / (2 * r)) - e / 2 * math.sqrt(4 * r**2 - e**2) print(len(crescents.faces), "faces, area", crescents.mass, "exact", 2 * (math.pi * r**2 - lens), "4 r e", 4 * r * e) ``` ```python nbsphinx="hide-input" cut_away = [f for f in crescents.faces if f.center[0] > 0] left_standing = [f for f in crescents.faces if f.center[0] < 0] Row((px.Compound([nominal, drilled]), "The nominal and the drilled bore", {"fill": False}), (crescents, "Cut away in red, left standing in cyan", {"fills": [(f, RED) for f in cut_away] + [(f, CYAN) for f in left_standing]}), caption="A bore drilled 0.5 off its position") ``` ## Many parts `px.SymmetricDifference` keeps the regions that an odd number of the parts covers, and like `px.Unify` it runs on all cores. This is the even-odd rule by which SVG and PostScript fill overlapping outlines. The example lays four horizontal bars of 80 × 10 over four vertical ones, every other row and column of a board of 8 × 8 squares of 10. A square lies in one bar where only its row or only its column has one, and in two bars, or none, where both or neither have one. The odd squares are those of a checkerboard, 32 of them, which touch only at their corners (Figure 3): ```python rows = [px.QuickSketch((0, 10 * k)).Rectangle(80, 10).Face() for k in (1, 3, 5, 7)] columns = [px.QuickSketch((10 * k, 0)).Rectangle(10, 80).Face() for k in (1, 3, 5, 7)] board = px.SymmetricDifference(rows + columns) print(len(board.faces), "faces, area", board.mass, "exact", 32 * 10**2) ``` ```python nbsphinx="hide-input" Row((px.Compound(rows + columns), "Four rows and four columns", {"fill": False}), (board, "Their symmetric difference"), caption="A checkerboard as the symmetric difference of eight bars") ``` See [Two faces](two-faces.md) for the other operations on two faces, and [Many faces at once](many-faces.md) for lists of faces on all cores.