Skip to content

Instantly share code, notes, and snippets.

@shmulc8
Created September 8, 2026 13:46
Show Gist options
  • Select an option

  • Save shmulc8/a675e7e3803626f8a70d2112ade7dd59 to your computer and use it in GitHub Desktop.

Select an option

Save shmulc8/a675e7e3803626f8a70d2112ade7dd59 to your computer and use it in GitHub Desktop.
A386493: proof and reproducible verifiers for a(13..16) = 65,65,89,89

A386493: a(13..16) = 65, 65, 89, 89

A reproducible exhaustive computational proof for four terms of OEIS A386493. Computation: September 8, 2026.

The problem is to minimize the squared diameter of a set of integer lattice points such that, at every selected point, its distances to all other selected points are different. This excludes collinear midpoint triples as well as ordinary isosceles triangles.

Result

Number of points Minimum squared diameter
13 65
14 65
15 89
16 89

Upper bounds: explicit constructions

The 14-point construction, of squared diameter 65, is

(1,2), (1,3), (1,5), (1,6), (4,0), (4,1), (4,7), (4,8),
(7,1), (7,7), (8,2), (8,3), (8,5), (8,6).

The 16-point construction, of squared diameter 89, is

(8,3), (7,0), (1,5), (1,6), (4,0), (8,2), (4,8), (8,5),
(2,0), (5,0), (2,8), (1,2), (1,3), (8,6), (7,8), (5,8).

Deleting the final point gives the 13- and 15-point constructions. All four are in witnesses.json. The verifier checks distinct integer coordinates, the exact diameter, every apex's distance list, and every triple.

Lower bounds: exhaustive exclusions

For squared diameter at most D, each coordinate span is at most floor(sqrt(D)). Translating both coordinate minima to zero therefore places every possible configuration in the complete square {0,...,floor(sqrt(D))}^2.

The programs enumerate increasing-index subsets of this entire domain; no additional symmetry restriction is imposed. A chosen point eliminates all future points too far from it and all future points that would form a forbidden triple with it and an earlier selected point. Each feasible subset occurs in the search. Branches with too few remaining candidates cannot contain a solution.

exact_search.rs also uses a proper graph-coloring upper bound. The graph connects pairs of remaining candidates that can coexist with the selected prefix. Each color class consists of mutually incompatible candidates, so a completion uses at most one point per color.

Program Excluded configuration Nodes visited
exact_search.rs 13 points, D <= 64 789,000
exact_search.rs 15 points, D <= 85 11,736,134
independent85.rs 15 points, D <= 85 46,676,991

The independent D=85 program uses candidate-count pruning only, with no coloring bound, and constructs the forbidden triples by separately testing all three apex positions. All three searches completed exhaustive exclusions. The programs distinguish timeout from a completed exclusion.

The D=64 exclusion gives a lower bound of 65 for 13 points and hence for 14 points. The D=85 exclusion also covers 16 points by taking a 15-point subset. None of 86, 87, or 88 is a sum of two integer squares, so there is no possible squared lattice diameter between 85 and 89. Consequently, 89 is the lower bound for both 15 and 16 points. The constructions attain these bounds.

Reproduce the complete result

Save all files in this Gist into one directory. Install Python 3.9+ and Rust, then run:

python3 verify.py

No Python packages or SAT solver are required. The script compiles both Rust programs, runs the three complete exclusions, checks the deterministic node counts, validates the four constructions, and writes verification.json. The source programs perform exact integer arithmetic. Reported runtimes are machine-dependent; an interrupted or timed-out run cannot establish a bound.

This is an exhaustive computational argument, not a proof-assistant certificate. An additional independently checked SAT/DRAT certificate for the D=64 exclusion is available in the original evidence repository. It is supplementary: the complete non-SAT replay above stands on its own.

//! Exhaustive subset search for A386493 on the translated finite square.
//!
//! This deliberately has no SAT dependency. A u128 is sufficient for the
//! 9x9 domain used at D <= 74 (and the implementation rejects larger domains).
use std::env;
use std::fs;
use std::time::{Duration, Instant};
#[derive(Clone, Copy)]
struct Pt { x: i32, y: i32 }
struct Search {
n: usize,
pts: Vec<Pt>,
compat: Vec<u128>,
forbidden: Vec<Vec<u128>>,
selected: Vec<usize>,
nodes: u64,
color_calls: u64,
started: Instant,
limit: Option<Duration>,
timed_out: bool,
witness: Option<Vec<usize>>,
}
fn bit(i: usize) -> u128 { 1u128 << i }
fn json_string(s: &str) -> String { format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")) }
impl Search {
fn expired(&mut self) -> bool {
if self.nodes & 1023 != 0 { return false; }
if self.limit.map_or(false, |x| self.started.elapsed() >= x) {
self.timed_out = true;
true
} else { false }
}
// Proper greedy coloring of the compatibility graph. A clique can use
// at most one vertex per color because each color is made pairwise
// incompatible. Prefix triple restrictions are included in edge tests.
fn coloring_bound(&mut self, candidates: u128) -> usize {
self.color_calls += 1;
let mut colors: Vec<Vec<usize>> = Vec::new();
let mut rest = candidates;
while rest != 0 {
let b = rest & (!rest + 1);
let v = b.trailing_zeros() as usize;
rest ^= b;
let mut placed = false;
for cls in colors.iter_mut() {
if cls.iter().all(|&w| !self.can_coexist(v, w)) {
cls.push(v);
placed = true;
break;
}
}
if !placed { colors.push(vec![v]); }
}
colors.len()
}
fn can_coexist(&self, a: usize, b: usize) -> bool {
if self.compat[a] & bit(b) == 0 { return false; }
self.selected.iter().all(|&u| self.forbidden[u][a] & bit(b) == 0)
}
fn dfs(&mut self, candidates: u128) {
if self.witness.is_some() || self.timed_out { return; }
self.nodes += 1;
if self.expired() { return; }
let have = self.selected.len();
if have == self.n {
self.witness = Some(self.selected.clone());
return;
}
let need = self.n - have;
if (candidates.count_ones() as usize) < need { return; }
if have + self.coloring_bound(candidates) < self.n { return; }
let mut rest = candidates;
while rest != 0 {
if self.witness.is_some() || self.timed_out { return; }
let b = rest & (!rest + 1);
rest ^= b;
let v = b.trailing_zeros() as usize;
// Descendants are strictly later indices, so every subset occurs
// once and no symmetry assumption is introduced.
let mut next = rest & self.compat[v];
for &u in &self.selected { next &= !self.forbidden[u][v]; }
self.selected.push(v);
self.dfs(next);
self.selected.pop();
}
}
}
fn isqrt(n: i32) -> i32 {
let mut x = 0;
while (x + 1) * (x + 1) <= n { x += 1; }
x
}
fn main() {
let args: Vec<String> = env::args().collect();
let mut n = None; let mut d = None; let mut out = None; let mut timeout = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--n" => { i += 1; n = Some(args[i].parse::<usize>().unwrap()); }
"--D" => { i += 1; d = Some(args[i].parse::<i32>().unwrap()); }
"--out" => { i += 1; out = Some(args[i].clone()); }
"--timeout-sec" => { i += 1; timeout = Some(args[i].parse::<u64>().unwrap()); }
_ => panic!("unknown argument {}", args[i]),
}
i += 1;
}
let n = n.expect("--n"); let d = d.expect("--D");
let side = isqrt(d);
let mut pts = Vec::new();
for x in 0..=side { for y in 0..=side { pts.push(Pt { x, y }); } }
assert!(pts.len() <= 128, "u128 candidate bitset requires <=128 points");
let m = pts.len();
let mut compat = vec![0u128; m];
let mut forbidden = vec![vec![0u128; m]; m];
let dist = |a: Pt, b: Pt| (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
for a in 0..m {
for b in 0..m {
if a != b && dist(pts[a], pts[b]) <= d { compat[a] |= bit(b); }
}
}
for a in 0..m { for b in (a + 1)..m { for w in 0..m {
if w == a || w == b { continue; }
let bad = dist(pts[a], pts[b]) == dist(pts[a], pts[w])
|| dist(pts[b], pts[a]) == dist(pts[b], pts[w])
|| dist(pts[w], pts[a]) == dist(pts[w], pts[b]);
if bad { forbidden[a][b] |= bit(w); forbidden[b][a] |= bit(w); }
}} }
let started = Instant::now();
let mut s = Search { n, pts, compat, forbidden, selected: Vec::new(),
nodes: 0, color_calls: 0, started, limit: timeout.map(Duration::from_secs), timed_out: false, witness: None };
let all = if m == 128 { u128::MAX } else { (1u128 << m) - 1 };
s.dfs(all);
let status = if s.witness.is_some() { "sat" } else if s.timed_out { "timeout" } else { "unsat" };
let exhaustive = status == "unsat";
let elapsed = s.started.elapsed().as_secs_f64();
let points = s.witness.as_ref().map(|ws| ws.iter().map(|&j| format!("[{},{}]", s.pts[j].x, s.pts[j].y)).collect::<Vec<_>>().join(","));
let result = format!("{{\n \"status\": {},\n \"exhaustive\": {},\n \"n\": {},\n \"D\": {},\n \"side\": {},\n \"points\": [{}],\n \"nodes\": {},\n \"coloring_calls\": {},\n \"elapsed_sec\": {:.6}\n}}\n",
json_string(status), exhaustive, n, d, side, points.unwrap_or_default(), s.nodes, s.color_calls, elapsed);
if let Some(path) = out { fs::write(path, &result).unwrap(); }
print!("{}", result);
}
use std::time::{Instant,Duration};
struct Search { adj:Vec<u128>, bad:Vec<Vec<u128>>, sel:Vec<usize>, nodes:u64, start:Instant, timeout:bool, witness:Option<Vec<usize>> }
impl Search {
fn dfs(&mut self, mut cand:u128) {
self.nodes+=1;
if self.nodes%4096==0 && self.start.elapsed()>Duration::from_secs(85) {self.timeout=true;return}
if self.sel.len()==15 {self.witness=Some(self.sel.clone());return}
while cand.count_ones() as usize + self.sel.len() >=15 {
let v=cand.trailing_zeros() as usize; cand &= cand-1;
let mut next=cand & self.adj[v];
for &u in &self.sel {next &= !self.bad[v][u]}
self.sel.push(v);self.dfs(next);self.sel.pop();
if self.timeout || self.witness.is_some(){return}
}
}
}
fn main(){
let m=100usize; let mut ds=vec![vec![0i32;m];m];
for i in 0..m {for j in 0..m {let dx=(i%10) as i32-(j%10) as i32;let dy=(i/10) as i32-(j/10) as i32;ds[i][j]=dx*dx+dy*dy;}}
let mut adj=vec![0u128;m];let mut bad=vec![vec![0u128;m];m];
for i in 0..m {for j in 0..m {if i!=j && ds[i][j]<=85 {adj[i]|=1<<j;}
if i==j {continue}
for k in 0..m {if k==i || k==j {continue}
let t=[i,j,k];let mut invalid=false;
for a in 0..3 {let apex=t[a];if ds[apex][t[(a+1)%3]]==ds[apex][t[(a+2)%3]] {invalid=true}}
if invalid {bad[i][j]|=1<<k}
}
}}
let mut s=Search{adj,bad,sel:vec![],nodes:0,start:Instant::now(),timeout:false,witness:None};s.dfs((1u128<<100)-1);
let status=if s.timeout {"timeout"}else if s.witness.is_some(){"sat"}else{"unsat"};
println!("{{\"status\":\"{}\",\"exhaustive\":{},\"nodes\":{},\"seconds\":{},\"D\":85,\"n\":15,\"domain_points\":100,\"pruning\":\"candidate count only\",\"witness_indices\":{:?}}}",status,status=="unsat",s.nodes,s.start.elapsed().as_secs_f64(),s.witness.unwrap_or_default());
}
34c3e761122c3094daed789131fce7dd62bfc1dac06741237064b845a15be495 00-README.md
bd9a4c2ec452cbdc527ee1e06604cfdf0fa33ff8d6588c7a726d95fb408392cb exact_search.rs
cb3c880504ebf094a5c2dab093dbce1adebfdf532b3bb947b3475d6ff7222ddf independent85.rs
c4605c4c49db1447d17bfea640df5e20d673bd38f8c5d61d4e279061139cfc12 verification.json
dc39535e305044c9a9b07f97517110ff18d890a3aff4815f50de8de3116575eb verify.py
84b5364cecf0d7fe41135e8c58d2fa6645c9dec4d12f28624e8b9533f4b987e9 witnesses.json
{
"witnesses": [
{
"n": 13,
"squared_diameter": 65,
"status": "PASS"
},
{
"n": 14,
"squared_diameter": 65,
"status": "PASS"
},
{
"n": 15,
"squared_diameter": 89,
"status": "PASS"
},
{
"n": 16,
"squared_diameter": 89,
"status": "PASS"
}
],
"exclusions": [
{
"source": "exact_search.rs",
"source_sha256": "bd9a4c2ec452cbdc527ee1e06604cfdf0fa33ff8d6588c7a726d95fb408392cb",
"result": {
"status": "unsat",
"exhaustive": true,
"n": 13,
"D": 64,
"side": 8,
"points": [],
"nodes": 789000,
"coloring_calls": 352780,
"elapsed_sec": 0.31106
}
},
{
"source": "exact_search.rs",
"source_sha256": "bd9a4c2ec452cbdc527ee1e06604cfdf0fa33ff8d6588c7a726d95fb408392cb",
"result": {
"status": "unsat",
"exhaustive": true,
"n": 15,
"D": 85,
"side": 9,
"points": [],
"nodes": 11736134,
"coloring_calls": 5415004,
"elapsed_sec": 5.713564
}
},
{
"source": "independent85.rs",
"source_sha256": "cb3c880504ebf094a5c2dab093dbce1adebfdf532b3bb947b3475d6ff7222ddf",
"result": {
"status": "unsat",
"exhaustive": true,
"nodes": 46676991,
"seconds": 0.576989875,
"D": 85,
"n": 15,
"domain_points": 100,
"pruning": "candidate count only",
"witness_indices": []
}
}
],
"status": "PASS",
"conclusion": {
"a(13)": 65,
"a(14)": 65,
"a(15)": 89,
"a(16)": 89
}
}
#!/usr/bin/env python3
"""Reproduce A386493(13..16) = 65,65,89,89. Python stdlib + rustc only."""
from pathlib import Path
from itertools import combinations
import hashlib
import json
import subprocess
import tempfile
HERE=Path(__file__).resolve().parent
def require(condition,message):
if not condition: raise RuntimeError(message)
def check_witness(c,n,D):
require(c['n']==n and c['D']==D,'Wrong witness metadata')
p=c['points']
require(len(p)==n and all(len(q)==2 and all(type(x) is int for x in q) for q in p),'Invalid coordinates')
require(len(set(map(tuple,p)))==n,'Repeated point')
def dist(a,b):return (a[0]-b[0])**2+(a[1]-b[1])**2
require(max(dist(a,b) for a,b in combinations(p,2))==D,'Wrong squared diameter')
for i,a in enumerate(p):
ds=[dist(a,b) for j,b in enumerate(p) if i!=j]
require(len(set(ds))==n-1,'Equal distances at one apex')
for triple in combinations(p,3):
require(len({dist(a,b) for a,b in combinations(triple,2)})==3,'Isosceles or midpoint triple')
return {'n':n,'squared_diameter':D,'status':'PASS'}
def main():
witnesses=json.loads((HERE/'witnesses.json').read_text())
require(len(witnesses)==4,'Expected four witnesses')
results={'witnesses':[check_witness(c,n,D) for c,(n,D) in zip(witnesses,[(13,65),(14,65),(15,89),(16,89)])],'exclusions':[]}
require({x*x+y*y for x in range(10) for y in range(10) if 85<x*x+y*y<=89}=={89},'Unexpected squared distance')
with tempfile.TemporaryDirectory(prefix='a386493-') as td:
for source in ['exact_search.rs','independent85.rs']:
binary=Path(td)/source.removesuffix('.rs')
subprocess.run(['rustc','-O',str(HERE/source),'-o',str(binary)],check=True,capture_output=True,text=True)
cases=[(13,64,789000),(15,85,11736134)] if source=='exact_search.rs' else [(15,85,46676991)]
for n,D,nodes in cases:
args=['--n',str(n),'--D',str(D),'--timeout-sec','90'] if source=='exact_search.rs' else []
p=subprocess.run([str(binary)]+args,check=True,capture_output=True,text=True,timeout=100)
r=json.loads(p.stdout)
require(r['status']=='unsat' and r['exhaustive'] is True,'Search did not complete an exclusion')
require((r['n'],r['D'],r['nodes'])==(n,D,nodes),'Unexpected exhaustive result')
results['exclusions'].append({'source':source,'source_sha256':hashlib.sha256((HERE/source).read_bytes()).hexdigest(),'result':r})
results.update(status='PASS',conclusion={'a(13)':65,'a(14)':65,'a(15)':89,'a(16)':89})
(HERE/'verification.json').write_text(json.dumps(results,indent=2)+'\n')
print(json.dumps(results,indent=2))
if __name__=='__main__':main()
[
{
"n": 13,
"D": 65,
"points": [
[
1,
2
],
[
1,
3
],
[
1,
5
],
[
1,
6
],
[
4,
0
],
[
4,
1
],
[
4,
7
],
[
4,
8
],
[
7,
1
],
[
7,
7
],
[
8,
2
],
[
8,
3
],
[
8,
5
]
]
},
{
"n": 14,
"D": 65,
"points": [
[
1,
2
],
[
1,
3
],
[
1,
5
],
[
1,
6
],
[
4,
0
],
[
4,
1
],
[
4,
7
],
[
4,
8
],
[
7,
1
],
[
7,
7
],
[
8,
2
],
[
8,
3
],
[
8,
5
],
[
8,
6
]
]
},
{
"n": 15,
"D": 89,
"points": [
[
8,
3
],
[
7,
0
],
[
1,
5
],
[
1,
6
],
[
4,
0
],
[
8,
2
],
[
4,
8
],
[
8,
5
],
[
2,
0
],
[
5,
0
],
[
2,
8
],
[
1,
2
],
[
1,
3
],
[
8,
6
],
[
7,
8
]
]
},
{
"n": 16,
"D": 89,
"points": [
[
8,
3
],
[
7,
0
],
[
1,
5
],
[
1,
6
],
[
4,
0
],
[
8,
2
],
[
4,
8
],
[
8,
5
],
[
2,
0
],
[
5,
0
],
[
2,
8
],
[
1,
2
],
[
1,
3
],
[
8,
6
],
[
7,
8
],
[
5,
8
]
]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment