240 lines
14 KiB
Python
240 lines
14 KiB
Python
"""Host-only experimental union event contract; no solver or runtime admission.
|
|
|
|
Consumes existing level-wide union geometry, never its legacy transfer tables.
|
|
The frozen 707afe6 rectangle oracle remains independent and unchanged. Topology
|
|
acceptance proves packet accounting, not interpolation accuracy or GPU support.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import spacetime_reference as ref
|
|
|
|
|
|
class UnresolvedTopology(ValueError):
|
|
"""An experimental coupling limitation, not a production geometry rejection."""
|
|
def __init__(self, code, **detail):
|
|
self.code, self.detail = code, detail
|
|
super().__init__(f"experimental space-time topology unresolved: {code}: {detail}")
|
|
|
|
|
|
def owned_slots(layout, directions, cells):
|
|
"""Map dense logical reads/writes to existing owner-core storage, not halos."""
|
|
cells = np.asarray(cells)
|
|
if not np.issubdtype(cells.dtype, np.integer) or np.any((cells < 0) | (cells >= np.prod(layout.shape))):
|
|
raise ValueError('invalid dense cell index')
|
|
owner = layout.owner.ravel()[cells]
|
|
if np.any(owner == layout.hole):
|
|
raise UnresolvedTopology('missing_block_owner', cells=cells[owner == layout.hole].tolist())
|
|
return layout.slot(owner, directions)
|
|
|
|
|
|
def _first_entry(covered, origin, child, c):
|
|
"""First continuous union entry of one characteristic, in exact quarter times.
|
|
|
|
Probe the cells immediately before/after each crossed grid line using integer
|
|
eighth-cell coordinates. Return only actual exterior faces of the UNION.
|
|
"""
|
|
start4 = 4*origin + 2*child + 1
|
|
for t4 in (1, 3):
|
|
before = (2*start4 + c*(2*t4-1))//8
|
|
after = (2*start4 + c*(2*t4+1))//8
|
|
if not covered[tuple(before)] and covered[tuple(after)]:
|
|
axes = []
|
|
for j in range(2):
|
|
if before[j] != after[j]:
|
|
neighbor = after.copy(); neighbor[j] = before[j]
|
|
if not covered[tuple(neighbor)]: axes.append(j)
|
|
return t4, axes
|
|
return None
|
|
|
|
|
|
def _crossing_time(covered, origin, position, c, axes):
|
|
"""Fig.11 convex T uses the last plane; Fig.13 concave T uses the first.
|
|
|
|
Both use the same KKT formulation on different boundary geometry.
|
|
DOI/primary PDF: https://www.osti.gov/servlets/purl/1226215, §3.5.1.
|
|
"""
|
|
times=[(origin[j]+int(c[j]>0)-position[j])/c[j] for j in axes]
|
|
if len(axes)==1: return times[0]
|
|
sides=[bool(covered[tuple(origin+np.eye(2,dtype=int)[j]*c[j])]) for j in axes]
|
|
if sides[0]!=sides[1]:
|
|
raise UnresolvedTopology('mixed_corner_support',source_yx=origin.tolist())
|
|
return min(times) if all(sides) else max(times)
|
|
|
|
|
|
class UnionPlan:
|
|
"""Two-level, level-wide square geometry; all coordinates are level-local.
|
|
|
|
Assumes a complete parent domain; does not validate nested parent coverage.
|
|
``pair`` is existing FixedPatchSet2D geometry, even for one rectangle. Scalar
|
|
window origins and nested state coverage need their own contract; they are
|
|
explicitly unadmitted here. No global rectangle-gap rule is imposed.
|
|
"""
|
|
phase = 'arrival_f'
|
|
runtime_supported = False
|
|
interpolation_accuracy = 'unqualified_for_new_unions'
|
|
|
|
def __init__(self, pair):
|
|
ny, nx = pair.coarse_shape
|
|
self.origin = tuple(pair.fine_origin)
|
|
if ny != nx or pair.fine_shape != (2*ny, 2*nx) or self.origin != (0., 0.):
|
|
raise UnresolvedTopology('requires_level_wide_square_pair',
|
|
coarse_shape=pair.coarse_shape, fine_shape=pair.fine_shape, origin=self.origin)
|
|
self.n, self.nf = nx, 2*nx
|
|
covered = np.array(pair.covered, dtype=bool, copy=True)
|
|
active = np.kron(covered, np.ones((2, 2), bool))
|
|
if not np.array_equal(active, pair.fine_kind == 0):
|
|
raise UnresolvedTopology('non_pair_active_coverage')
|
|
self.maps = SimpleNamespace(coarse_active=~covered, coarse_covered=covered, fine_active=active)
|
|
self.outgoing, transit = [[], []], []
|
|
# Guzik et al. §3.5.4, Fig.19 (https://www.osti.gov/servlets/purl/1226215):
|
|
# retain substep-0 outgoing G at source, then fill the substep-1 ghost.
|
|
# Rows are (q, fine source, next ghost); no covered coarse receiver credit.
|
|
for q in range(1, 9):
|
|
c = np.array((ref.cpu.CY[q], ref.cpu.CX[q]))
|
|
boundary = active & ~np.roll(active, tuple(-c), axis=(0, 1))
|
|
for y, x in zip(*np.nonzero(boundary)):
|
|
p = np.array((y, x))
|
|
for m in (0, 1):
|
|
target = (p+(2-m)*c)//2
|
|
if covered[tuple(target)]:
|
|
if m != 0 or not active[tuple(p+2*c)]:
|
|
raise UnresolvedTopology('unsupported_transit', q=q, substep=m, source_yx=p.tolist())
|
|
ghost=p+c
|
|
transit.append((q,int(y*self.nf+x),int(ghost[0]*self.nf+ghost[1])))
|
|
else:
|
|
self.outgoing[m].append((q, int(y*self.nf+x), int(target[0]*nx+target[1])))
|
|
self.outgoing = [np.array(x, dtype=np.int64).reshape(-1, 3) for x in self.outgoing]
|
|
self.transit = np.array(transit,dtype=np.int64).reshape(-1,3)
|
|
self.groups = []
|
|
for iy, ix in zip(*np.nonzero(~covered)):
|
|
origin = np.array((iy, ix))
|
|
# Two-cell production margins make all nearby probes in bounds.
|
|
if not covered[max(0,iy-1):iy+2,max(0,ix-1):ix+2].any(): continue
|
|
for q in range(1, 9):
|
|
c = np.array((ref.cpu.CY[q], ref.cpu.CX[q]))
|
|
children = [np.array((dy, dx)) for dy in (0, 1) for dx in (0, 1)]
|
|
starts = [2*origin+child for child in children]
|
|
events = []
|
|
for p in starts:
|
|
event = None
|
|
for m in (0, 1):
|
|
old, new = p+m*c, p+(m+1)*c
|
|
if not active[tuple(old)] and active[tuple(new)]:
|
|
event = (m, int(old[0]*self.nf+old[1])); break
|
|
events.append(event)
|
|
if all(e is None for e in events): continue
|
|
entries = [_first_entry(covered, origin, child, c) for child in children]
|
|
axes = sorted({j for entry in entries if entry is not None for j in entry[1]})
|
|
if not axes:
|
|
raise UnresolvedTopology('missing_exterior_support', source_yx=origin.tolist(), q=q)
|
|
points = []
|
|
for p, entry, event in zip(starts, entries, events):
|
|
position = (p+.5)/2
|
|
time = _crossing_time(covered,origin,position,c,axes)
|
|
if entry is not None and (4*time != entry[0] or event is None):
|
|
raise UnresolvedTopology('nonconvex_entry_support', source_yx=origin.tolist(), q=q,
|
|
quarter_time=entry[0], support_quarter_time=float(4*time))
|
|
offset = position+c*time-(origin+.5+c*.5)
|
|
points.append((float(offset[1]), float(offset[0]), float(time-.5)) if len(axes)==2
|
|
else (float(offset[1-axes[0]]), float(time-.5)))
|
|
kind = 'corner' if len(axes)==2 else 'face'; points = tuple(points)
|
|
samples, _, evaluation, coefficients = ref.interpolation(kind, points)
|
|
indices = []
|
|
for row in samples:
|
|
shift = np.array((row[1],row[0]), int) if kind=='corner' else np.eye(2,dtype=int)[1-axes[0]]*int(row[0])
|
|
pos = (origin+shift) % nx; indices.append(int(pos[0]*nx+pos[1]))
|
|
target = (origin+c) % nx
|
|
self.groups.append(ref.Group(q,int(iy*nx+ix),int(target[0]*nx+target[1]),kind,
|
|
points,np.array(indices),evaluation@coefficients,tuple(events)))
|
|
receivers = {(int(q),int(target)) for rows in self.outgoing for q,_,target in rows}
|
|
receivers |= {(g.q,g.receiver) for g in self.groups if None in g.events}
|
|
self.receivers = np.array(sorted(receivers),dtype=np.int64).reshape(-1,2)
|
|
self.proof = validate_ownership(self)
|
|
|
|
|
|
def validate_ownership(plan):
|
|
"""Independent mask-based event coverage and four-quarter receiver accounting."""
|
|
n, nf, active = plan.n, plan.nf, plan.maps.fine_active
|
|
actual = [(m,g.q,slot) for g in plan.groups for e in g.events if e is not None for m,slot in [e]]
|
|
expected_transit={(q,int(y*nf+x),int((y+ref.cpu.CY[q])*nf+x+ref.cpu.CX[q]))
|
|
for q in range(1,9) for y,x in zip(*np.nonzero(active))
|
|
if not active[y+ref.cpu.CY[q],x+ref.cpu.CX[q]]
|
|
and active[y+2*ref.cpu.CY[q],x+2*ref.cpu.CX[q]]}
|
|
routes=[tuple(map(int,row)) for row in plan.transit]
|
|
if len(routes)!=len(set(routes)) or set(routes)!=expected_transit:
|
|
raise ValueError('transit provenance/uniqueness/coverage failed')
|
|
actual += [(1,q,ghost) for q,source,ghost in routes]
|
|
expected = {(m,q,int((y-ref.cpu.CY[q])*nf+x-ref.cpu.CX[q]))
|
|
for m in (0,1) for q in range(1,9) for y,x in zip(*np.nonzero(active))
|
|
if not active[y-ref.cpu.CY[q],x-ref.cpu.CX[q]]}
|
|
if len(actual)!=len(set(actual)) or set(actual)!=expected:
|
|
raise ValueError('incoming writer uniqueness/coverage failed')
|
|
packets = {tuple(map(int,row)):[] for row in plan.receivers}
|
|
if len(packets)!=len(plan.receivers): raise ValueError('duplicate coarse receiver')
|
|
anchors = set()
|
|
for i,g in enumerate(plan.groups):
|
|
if not 1<=g.q<9 or not 0<=g.source<n*n or not plan.maps.coarse_active.ravel()[g.source]:
|
|
raise ValueError('invalid coarse source anchor')
|
|
if (g.q,g.source) in anchors: raise ValueError('duplicate coarse source group')
|
|
anchors.add((g.q,g.source))
|
|
origin=np.array(divmod(g.source,n)); c=np.array((ref.cpu.CY[g.q],ref.cpu.CX[g.q]))
|
|
target=(origin+c)%n
|
|
if g.receiver!=target[0]*n+target[1]: raise ValueError('group receiver provenance failed')
|
|
ordered=[]; entries=[]
|
|
for dy,dx in ((0,0),(0,1),(1,0),(1,1)):
|
|
p=2*origin+(dy,dx); event=None
|
|
for m in (0,1):
|
|
old,new=p+m*c,p+(m+1)*c
|
|
if not active[tuple(old)] and active[tuple(new)]:
|
|
event=(m,int(old[0]*nf+old[1])); break
|
|
ordered.append(event)
|
|
entries.append(_first_entry(plan.maps.coarse_covered,origin,np.array((dy,dx)),c))
|
|
if tuple(ordered)!=g.events or all(e is None for e in ordered):
|
|
raise ValueError('ordered child characteristic provenance failed')
|
|
axes=sorted({j for e in entries if e is not None for j in e[1]})
|
|
if not axes or g.kind!=('corner' if len(axes)==2 else 'face'):
|
|
raise ValueError('group supporting axes differ')
|
|
samples,_,evaluation,coefficients=ref.interpolation(g.kind,g.points)
|
|
indices=[]
|
|
for row in samples:
|
|
shift=np.array((row[1],row[0]),int) if g.kind=='corner' else np.eye(2,dtype=int)[1-axes[0]]*int(row[0])
|
|
pos=(origin+shift)%n; indices.append(int(pos[0]*n+pos[1]))
|
|
if not np.array_equal(g.samples,indices): raise ValueError('sample anchor provenance failed')
|
|
for k,(dy,dx) in enumerate(((0,0),(0,1),(1,0),(1,1))):
|
|
position=origin+(np.array((dy,dx))+.5)/2
|
|
time=_crossing_time(plan.maps.coarse_covered,origin,position,c,axes)
|
|
offset=position+c*time-(origin+.5+c*.5)
|
|
point=(float(offset[1]),float(offset[0]),float(time-.5)) if g.kind=='corner' else (float(offset[1-axes[0]]),float(time-.5))
|
|
if g.points[k]!=point: raise ValueError('evaluation point provenance failed')
|
|
source=np.zeros(len(g.samples)); source[0 if g.kind=='corner' else 1]=1
|
|
if (not np.allclose(g.weights,evaluation@coefficients,rtol=0,atol=2e-14)
|
|
or not np.allclose(g.weights.mean(axis=0),source,rtol=0,atol=2e-14)):
|
|
raise ValueError('group coefficients/directional budget failed')
|
|
for k,event in enumerate(g.events):
|
|
if event is None:
|
|
if (g.q,g.receiver) not in packets: raise ValueError('missing leftover receiver')
|
|
packets[(g.q,g.receiver)].append(('leftover',i,k))
|
|
for m,rows in enumerate(plan.outgoing):
|
|
expected_out = {(q,int(y*nf+x),int(((y+(2-m)*ref.cpu.CY[q])//2)*n+(x+(2-m)*ref.cpu.CX[q])//2))
|
|
for q in range(1,9) for y,x in zip(*np.nonzero(active))
|
|
if not active[y+ref.cpu.CY[q],x+ref.cpu.CX[q]]
|
|
and plan.maps.coarse_active[(y+(2-m)*ref.cpu.CY[q])//2,(x+(2-m)*ref.cpu.CX[q])//2]}
|
|
actual_out = [tuple(map(int,row)) for row in rows]
|
|
if len(actual_out)!=len(set(actual_out)) or set(actual_out)!=expected_out:
|
|
raise ValueError('outgoing uniqueness/coverage failed')
|
|
for q,slot,target in actual_out: packets[(q,target)].append(('outgoing',m,q,slot))
|
|
expected_receivers = {(q,int(y*n+x)) for y,x in zip(*np.nonzero(plan.maps.coarse_active))
|
|
for q in range(1,9) if any(active[2*y+dy-ref.cpu.CY[q],2*x+dx-ref.cpu.CX[q]]
|
|
for dy in (0,1) for dx in (0,1)
|
|
if 0<=2*y+dy-ref.cpu.CY[q]<nf and 0<=2*x+dx-ref.cpu.CX[q]<nf)}
|
|
if set(packets)!=expected_receivers: raise ValueError('coarse receiver coverage failed')
|
|
tokens=[token for row in packets.values() for token in row]
|
|
if any(len(row)!=4 for row in packets.values()) or len(tokens)!=len(set(tokens)):
|
|
raise ValueError('receiver needs four distinct quarter-packets')
|
|
return dict(topology='supported',incoming_events=len(actual),outgoing_events=sum(map(len,plan.outgoing)),
|
|
transit_events=len(routes),
|
|
groups=len(plan.groups),receivers=len(packets),receiver_quarter_packets=len(tokens),
|
|
interpolation_accuracy=plan.interpolation_accuracy,runtime_supported=False,
|
|
scope='pair-local ownership/coefficients; complete parent domain assumed, nested coverage unvalidated')
|