diff --git a/package-lock.json b/package-lock.json index ba6d76d..6307cbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "apvd", "version": "0.1.0", "dependencies": { - "apvd": "https://gitpkg.now.sh/runsascoded/shapes?1e0bd5c", + "apvd": "https://gitpkg.now.sh/runsascoded/shapes?9928b7c", "bootstrap": "^5.3.1", "buffer": "^6.0.3", "lodash": "^4.17.21", @@ -3364,8 +3364,8 @@ }, "node_modules/apvd": { "version": "0.1.0", - "resolved": "https://gitpkg.now.sh/runsascoded/shapes?1e0bd5c", - "integrity": "sha512-XFPxxWn39MAqUn+7ch+bkOV9RtlQXrOipAFwqUZRWYAcqidQ9aKaeyxp+kwCIexA6nfn6MeDv8wUhsZ8IAVLnA==" + "resolved": "https://gitpkg.now.sh/runsascoded/shapes?9928b7c", + "integrity": "sha512-58g7kRoJJx8I2dMr7p/M2asgg8Ahoms7VuQzFtDG5PQJ4e03uj2DHZ1c6+VRNQFhEBo1hnHYXq6GEJlUEHaQqg==" }, "node_modules/argparse": { "version": "2.0.1", @@ -16288,8 +16288,8 @@ } }, "apvd": { - "version": "https://gitpkg.now.sh/runsascoded/shapes?1e0bd5c", - "integrity": "sha512-XFPxxWn39MAqUn+7ch+bkOV9RtlQXrOipAFwqUZRWYAcqidQ9aKaeyxp+kwCIexA6nfn6MeDv8wUhsZ8IAVLnA==" + "version": "https://gitpkg.now.sh/runsascoded/shapes?9928b7c", + "integrity": "sha512-58g7kRoJJx8I2dMr7p/M2asgg8Ahoms7VuQzFtDG5PQJ4e03uj2DHZ1c6+VRNQFhEBo1hnHYXq6GEJlUEHaQqg==" }, "argparse": { "version": "2.0.1", diff --git a/package.json b/package.json index 0944b91..41ab4a9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "email": "ryan@runsascoded.com" }, "dependencies": { - "apvd": "https://gitpkg.now.sh/runsascoded/shapes?1e0bd5c", + "apvd": "https://gitpkg.now.sh/runsascoded/shapes?9928b7c", "bootstrap": "^5.3.1", "buffer": "^6.0.3", "lodash": "^4.17.21", diff --git a/pages/ellipse-test.tsx b/pages/ellipse-test.tsx index c6e689d..16e891f 100644 --- a/pages/ellipse-test.tsx +++ b/pages/ellipse-test.tsx @@ -110,6 +110,16 @@ export function Body() { return
+ + + path).join(" ")}`} fillRule={"evenodd"} diff --git a/pages/index.tsx b/pages/index.tsx index 32e5321..57c9019 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -4,7 +4,7 @@ import Grid, {GridState} from "../src/components/grid" import React, {DetailedHTMLProps, Dispatch, HTMLAttributes, ReactNode, useCallback, useEffect, useMemo, useRef, useState} from "react" import * as apvd from "apvd" import {train, update_log_level} from "apvd" -import {makeModel, Model, Region, Step} from "../src/lib/regions" +import {makeModel, Model, Region, regionPath, Step} from "../src/lib/regions" import {Point} from "../src/components/point" import css from "./index.module.scss" import A from "next-utils/a" @@ -20,7 +20,7 @@ import {getMidpoint, getPointAndDirectionAtTheta, getRegionCenter} from "../src/ import {BoundingBox, getRadii, mapShape, S, Shape, shapeBox, Shapes, shapesParam, shapeStrJS, shapeStrJSON, shapeStrRust} from "../src/lib/shape"; import {TargetsTable} from "../src/components/tables/targets"; import {makeTargets, Target, Targets, targetsParam} from "../src/lib/targets"; -import {Disjoint, Ellipses4, Ellipses4t, InitialLayout, CirclesFlexible, toShape, CirclesFixed, Concentric} from "../src/lib/layout"; +import {Disjoint, Ellipses4, Ellipses4t, InitialLayout, CirclesFlexible, toShape, CirclesFixed, Nested} from "../src/lib/layout"; import {VarsTable} from "../src/components/tables/vars"; import {SparkLineProps} from "../src/components/spark-lines"; import {CircleCoords, Coord, makeVars, Vars, XYRRCoords, XYRRTCoords} from "../src/lib/vars"; @@ -257,7 +257,7 @@ const layouts: LinkItem[] = [ { name: "Circles (flexible)", val: CirclesFlexible, description: "4 ellipses, initialized as circles, and oriented in a diamond configuration, such that 2 different subsets (of 3) are symmetric, and 11 of 15 possible regions are represented (missing 2 4C2's and 2 4C3's).", }, { name: "Circles (fixed)", val: CirclesFixed, description: "4 circles, initialized in a diamond as in \"Circles (flexible)\" above, but these are fixed as circles (rx and ry remain constant, rotation is immaterial)", }, { name: "Disjoint", val: Disjoint, description: "4 disjoint circles. When two (or more) sets are supposed to intersect, but don't, a synthetic penalty is added to the error computation, which is proportional to: 1) each involved set's distance to the centroid of the centers of the sets that are supposed to intersect, as well as 2) the size of the target subset. This \"disjoint\" initial layout serves demonstrate/test this behavior. More sophisticated heuristics would be useful here, as the current scheme is generally insufficient to coerce all sets into intersecting as they should." }, - { name: "Concentric", val: Concentric, description: "4 concentric circles, stress test disjoint/contained region handling" }, + { name: "Nested", val: Nested, description: "4 nested circles, stresses disjoint/contained region handling, which has known issues!" }, { name: "Variant callers (best)", val: "#s=Mzxv4Cc95664TAhIgtTaZ1wTbpB32hca6RnYrxzN5QRgbF4oaXr5MStC6KxNYYZy5g5IuzaS1moF4lLWtIXXY-VOO2f8wNvsQk9Jqqfg0B-RDkXMZTCTpTaymPnuwF-vswFGRVwFE4hgScC1ofXRaBdnvzm84fjZ8wtEkWHaqiifUM4TVEtIbh8&t=633,618,112,187,0,14,1,319,13,55,17,21,0,9,36", description: <>Best computed layout for the "variant callers" example, from {VariantCallersPaperLink}. ≈50,000 steps beginning from the "Circles" layout above, error <0.176%.}, { name: "Variant callers (alternate)", val: "#s=MzC1VAFocttl2gbaDkR1obVIOSo-npdk8mfAn4j0s68wpq4FE4o0YIptFI5hupi525mqCJLTS0BbLsnqcJ0oFOtaun28Afy9HfyAHhdHhtsAsLO8mNdyKFNwt4op_97d4DXguxY3S4k7RxPbNbPIu_2XIvm5qJ0NJn5qsgeVxEhvcgoRO8FFnpU&t=633,618,112,187,0,14,1,319,13,55,17,21,0,9,36", description: <>Another layout for the "variant callers" example, from {VariantCallersPaperLink}. ≈20,000 steps beginning from the "Ellipses" layout above, error ≈2.27%.} // { name: "CircleLattice", layout: SymmetricCircleLattice, description: "4 circles centered at (0,0), (0,1), (1,0), (1,1)", }, @@ -383,7 +383,7 @@ export function Body() { () => { const targets = rawTargets const { numShapes } = targets - const initialSets = + const initialSets: S[] = initialShapes .slice(0, numShapes) .map((s, idx) => { @@ -507,9 +507,10 @@ export function Body() { () => { if (typeof window !== 'undefined') { window.model = model + window.curStep = curStep } }, - [ model,] + [ model, curStep ] ) const getHistoryState = useCallback( @@ -1219,28 +1220,9 @@ export function Body() { const regionPaths = useMemo( () => curStep && { - curStep.regions.map(({ key, segments}, regionIdx) => { - let d = '' - segments.forEach(({edge, fwd}, idx) => { - const { set: { shape }, node0, node1, theta0, theta1, } = edge - const [rx, ry] = getRadii(shape) - const theta = shape.kind === 'XYRRT' ? shape.t : 0 - const degrees = theta * 180 / PI - const [startNode, endNode] = fwd ? [node0, node1] : [node1, node0] - const start = {x: startNode.x.v, y: startNode.y.v} - const end = {x: endNode.x.v, y: endNode.y.v} - if (idx == 0) { - d = `M ${start.x} ${start.y}` - } - // console.log("edge:", edge, "fwd:", fwd, "theta0:", theta0, "theta1:", theta1, "start:", start, "end:", end, "shape:", shape, "degrees:", degrees) - if (segments.length == 1) { - const mid = getMidpoint(edge, 0.4) - d += ` A ${rx},${ry} ${degrees} 0 ${fwd ? 1 : 0} ${mid.x},${mid.y}` - d += ` A ${rx},${ry} ${degrees} 1 ${fwd ? 1 : 0} ${end.x},${end.y}` - } else { - d += ` A ${rx},${ry} ${degrees} ${theta1 - theta0 > PI ? 1 : 0} ${fwd ? 1 : 0} ${end.x},${end.y}` - } - }) + curStep.regions.map((region, regionIdx) => { + const { key, segments} = region + const d = regionPath(region) const isHovered = hoveredRegion == key return ( setHoveredRegion(key)} // onMouseLeave={() => setHoveredRegion(null)} onMouseOut={() => setHoveredRegion(null)} diff --git a/src/components/grid.tsx b/src/components/grid.tsx index fec73d5..0a4b7b3 100644 --- a/src/components/grid.tsx +++ b/src/components/grid.tsx @@ -277,14 +277,14 @@ export default function Grid({ handleMouseDown, handleMouseMove, handleDrag, han `${type}(${x},${y})`).join(" ")} > {gridLines} diff --git a/src/lib/layout.ts b/src/lib/layout.ts index be73f71..1509dbb 100644 --- a/src/lib/layout.ts +++ b/src/lib/layout.ts @@ -51,11 +51,12 @@ export const Disjoint: InitialLayout = [ { c: { x: 0, y: 3, }, r: { x: 1, y: 1 }, t: 0, }, ] -export const Concentric: InitialLayout = [ - { c: { x: 0, y: 0, }, r: { x: 1, y: 1 }, t: 0, }, +// TODO: if they actually share a center, the missing region penalty heuristics go to NaN +export const Nested: InitialLayout = [ + { c: { x: 0 , y: 0, }, r: { x: 1, y: 1 }, t: 0, }, { c: { x: 0.5, y: 0, }, r: { x: 2, y: 2 }, t: 0, }, - { c: { x: 0, y: 0, }, r: { x: 3, y: 3 }, t: 0, }, - { c: { x: 0, y: 0, }, r: { x: 4, y: 4 }, t: 0, }, + { c: { x: 1 , y: 0, }, r: { x: 3, y: 3 }, t: 0, }, + { c: { x: 1.5, y: 0, }, r: { x: 4, y: 4 }, t: 0, }, ] export const SymmetricCircleLattice: InitialLayout = [ diff --git a/src/lib/regions.ts b/src/lib/regions.ts index a25257e..2cc4790 100644 --- a/src/lib/regions.ts +++ b/src/lib/regions.ts @@ -1,6 +1,8 @@ import * as apvd from "apvd"; -import {Dual, Error, Targets} from "apvd" -import {S, Set, Shape} from "./shape"; +import {Dual, Targets} from "apvd" +import {getRadii, S, Set, Shape} from "./shape"; +import {PI} from "./math"; +import {getMidpoint} from "./region"; export type Point = { x: Dual @@ -8,7 +10,7 @@ export type Point = { edges: Edge[] } -export type Errors = Map +export type Errors = Map export type Step = { sets: S[] @@ -49,13 +51,26 @@ export function makeStep(step: apvd.Step, initialSets: Set[]): Step { const { components, errors, ...rest } = step const sets: S[] = [] components.forEach(c => c.sets.forEach(({ idx, shape }) => { - sets[idx] = { ...initialSets[idx], shape: makeShape(shape) } + sets[idx] = { ...initialSets[idx], shape: makeShape(shape), } })) const newComponents = components.map(c => makeComponent(c, sets)) + const newComponentsMap = new Map(newComponents.map(c => [c.key, c])) // console.log("initial sets:", sets) const points = newComponents.flatMap(c => c.points) const edges = newComponents.flatMap(c => c.edges) const regions = newComponents.flatMap(c => c.regions) + newComponents.forEach((newComponent, idx) => { + const apvdComponent = components[idx] + apvdComponent.regions.forEach((apvdRegion, regionIdx) => { + const newRegion = newComponent.regions[regionIdx] + // console.log("region:", apvdRegion, "children:", apvdRegion.child_component_keys) + newRegion.childComponents = apvdRegion.child_component_keys.map(key => { + const newComponent = newComponentsMap.get(key) + if (!newComponent) throw Error(`no component with key ${key}; referenced in region ${apvdRegion.key} of component ${apvdComponent.key}`) + return newComponent + }) + }) + }) return { sets, points, @@ -81,34 +96,52 @@ export function makeShape(shape: apvd.Shape): Shape { } } +export function makeRegion(region: apvd.Region, allSets: S[], edges: Edge[]): Region { + // console.log("makeRegion:", region) + const segments = region.segments.map(({ edge_idx, fwd }) => ({ + edge: edges[edge_idx], + fwd, + })) + const containers = region.container_set_idxs.map(idx => allSets[idx]) + return { + key: region.key, + segments, + area: region.area, + containers, + childComponents: [], + } +} + export function makeComponent(component: apvd.Component, allSets: S[]): Component { // console.log("makeComponent:", component) const points: Point[] = component.points.map(({ p: { x, y }}) => ({ x, y, edges: [] })) - const edges: Edge[] = component.edges.map(({ set_idx, node0_idx, node1_idx, theta0, theta1, container_idxs, is_component_boundary, }) => ({ - set: allSets[set_idx], - node0: points[node0_idx], - node1: points[node1_idx], - theta0, theta1, - containers: container_idxs.map(setIdx => allSets[setIdx]), - isComponentBoundary: is_component_boundary, - })) + const edges: Edge[] = component.edges.map(({ set_idx, node0_idx, node1_idx, theta0, theta1, container_idxs, is_component_boundary, }) => { + const node0 = points[node0_idx] + const node1 = points[node1_idx] + if (!node0 || !node1) { + throw Error(`node0 or node1 is null: ${node0_idx} ${node1_idx}, ${node0} ${node1}`) + } + return ({ + set: allSets[set_idx], + node0: points[node0_idx], + node1: points[node1_idx], + theta0, theta1, + containers: container_idxs.map(setIdx => allSets[setIdx]), + isComponentBoundary: is_component_boundary, + }) + }) component.points.forEach(({ edge_idxs }, pointIdx) => { const point = points[pointIdx] point.edges = edge_idxs.map(edgeIdx => edges[edgeIdx]) }) - const regions: Region[] = component.regions.map(({ key, segments, area, container_idxs }) => ({ - key, - segments: segments.map(({ edge_idx, fwd }) => ({ - edge: edges[edge_idx], - fwd, - })), - area, - containers: container_idxs.map((cidx: number) => allSets[cidx]), - })) - return { sets: component.sets.map(({ idx }) => allSets[idx]), points, edges, regions, } + const regions: Region[] = component.regions.map(r => makeRegion(r, allSets, edges)) + // const containers = component.container_idxs.map((cidx: number) => allSets[cidx]) + const sets = component.sets.map(({ idx }) => allSets[idx]) + const hull = makeRegion(component.hull, allSets, edges) + return { key: component.key, sets, points, edges, regions, /*containers, */hull, } } export type Region = { @@ -116,6 +149,35 @@ export type Region = { segments: Segment[] area: Dual containers: S[] + childComponents: Component[] +} + +export function regionPath({ segments, childComponents, }: Region): string { + let d = '' + segments.forEach(({edge, fwd}, idx) => { + const { set: { shape }, node0, node1, theta0, theta1, } = edge + const [rx, ry] = getRadii(shape) + const theta = shape.kind === 'XYRRT' ? shape.t : 0 + const degrees = theta * 180 / PI + const [startNode, endNode] = fwd ? [node0, node1] : [node1, node0] + const start = { x: startNode.x.v, y: startNode.y.v } + const end = { x: endNode.x.v, y: endNode.y.v } + if (idx == 0) { + d = `M ${start.x} ${start.y}` + } + // console.log("edge:", edge, "fwd:", fwd, "theta0:", theta0, "theta1:", theta1, "start:", start, "end:", end, "shape:", shape, "degrees:", degrees) + if (segments.length == 1) { + const mid = getMidpoint(edge, 0.4) + d += ` A ${rx},${ry} ${degrees} 0 ${fwd ? 1 : 0} ${mid.x},${mid.y}` + d += ` A ${rx},${ry} ${degrees} 1 ${fwd ? 1 : 0} ${end.x},${end.y}` + } else { + d += ` A ${rx},${ry} ${degrees} ${theta1 - theta0 > PI ? 1 : 0} ${fwd ? 1 : 0} ${end.x},${end.y}` + } + }) + if (childComponents?.length) { + d += ` ${childComponents.map(c => regionPath(c.hull)).join(' ')}` + } + return d } export interface Segment { @@ -134,8 +196,10 @@ export type Edge = { } export type Component = { + key: string sets: S[] points: Point[] edges: Edge[] regions: Region[] + hull: Region } diff --git a/src/lib/shape.ts b/src/lib/shape.ts index 1c9b5df..8f157c7 100644 --- a/src/lib/shape.ts +++ b/src/lib/shape.ts @@ -2,6 +2,7 @@ import {R2} from "apvd"; import {abs, cos, max, sin} from "./math"; import {Param} from "next-utils/params"; import ShapesBuffer, {Opts, ShapesParam} from "./shapes-buffer"; +import {Component} from "./regions"; export interface Circle { kind: 'Circle'