feat: 导入 IRSTD 研究代码与文档(小目标检测)
- 新增 irstd 核心包、scripts、tests、configs - 新增研究文档:literature_review / mathematical_analysis / novelty_matrix / experiment_plan / research_decision / research_log / manuscript_outline - 新增 evidence、results 数据与审计产物 - 新增 pyproject.toml、requirements、.gitignore
This commit is contained in:
1 parent
9b4a29c4d2
commit
04bf90c09b
35 files changed
+2220
-2
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import itertools,unittest,math
|
||||
import numpy as np
|
||||
from irstd.core import *
|
||||
|
||||
class CoreTests(unittest.TestCase):
|
||||
def test_matching_not_greedy(self):
|
||||
self.assertEqual(len(matching([[1,1],[1,0]])),2)
|
||||
def test_empty_matching(self):
|
||||
self.assertEqual(matching(np.zeros((0,2),bool)),[])
|
||||
self.assertEqual(matching(np.zeros((2,0),bool)),[])
|
||||
def test_adjacent_targets_conflict(self):
|
||||
_,selected=exact_mwis([.9,.8],[(0,1)])
|
||||
self.assertEqual(len(selected),1)
|
||||
self.assertEqual(len(exact_mwis([.9,.8],[])[1]),2)
|
||||
def test_invalid_recall_even_with_feasible_target(self):
|
||||
self.assertEqual(exact_mwis([1.,2.],[(0,1)])[1],[1])
|
||||
def test_pixel_threshold_splits_false_object(self):
|
||||
p=np.array([[.9,.6,.9]])
|
||||
self.assertEqual(len(components(p>=.5)),1)
|
||||
self.assertEqual(len(components(p>=.8)),2)
|
||||
def test_matching_deficiency_exhaustive(self):
|
||||
# Exhaust all 3x2 eligibility graphs and all subsets.
|
||||
for flat in itertools.product([0,1],repeat=6):
|
||||
e=np.array(flat).reshape(3,2)
|
||||
for bits in itertools.product([0,1],repeat=3):
|
||||
s=np.flatnonzero(bits);fp=len(s)-len(matching(e[s]))
|
||||
for j in set(range(3))-set(s):
|
||||
t=np.append(s,j);self.assertLessEqual(fp,len(t)-len(matching(e[t])))
|
||||
def test_canonical_bound_exhaustive(self):
|
||||
for flat in itertools.product([0,1],repeat=6):
|
||||
e=np.array(flat).reshape(3,2);pos={i for i,_ in matching(e)}
|
||||
for bits in itertools.product([0,1],repeat=3):
|
||||
s=np.flatnonzero(bits)
|
||||
self.assertLessEqual(len(s)-len(matching(e[s])),len(set(s)-pos))
|
||||
def test_surrogate_upper_bound(self):
|
||||
s=np.array([-100,-1,0,1,100.])
|
||||
self.assertGreaterEqual(softplus_bound(s,np.ones(5,bool)),(s>=0).sum())
|
||||
def test_cp_zero_and_all(self):
|
||||
self.assertAlmostEqual(cp_upper(0,100,.05),1-.05**.01)
|
||||
self.assertEqual(cp_upper(100,100,.05),1.)
|
||||
def test_small_calibration_abstains(self):
|
||||
r=calibrate(np.zeros((80,101),int),np.linspace(0,1,101),64,alpha=.01,risk='any')
|
||||
self.assertTrue(r['reject_all'])
|
||||
def test_large_zero_calibration_certifies_any(self):
|
||||
r=calibrate(np.zeros((1000,2),int),[.1,.9],64,alpha=.01,risk='any')
|
||||
self.assertFalse(r['reject_all']);self.assertEqual(r['threshold'],.1)
|
||||
def test_count_and_any_distinct(self):
|
||||
counts=np.zeros((1000,2),int)
|
||||
self.assertTrue(calibrate(counts,[.1,.9],64,.01,risk='count')['reject_all'])
|
||||
self.assertFalse(calibrate(counts,[.1,.9],64,.01,risk='any')['reject_all'])
|
||||
def test_invalid_calibration(self):
|
||||
for x in (np.array([[65]]),np.array([[.5]]),np.array([[np.nan]])):
|
||||
with self.assertRaises(ValueError):calibrate(x,[.5],64)
|
||||
def test_fixed_sequence_zero_boundary(self):
|
||||
for n,expected in ((298,True),(299,False)):
|
||||
r=calibrate(np.zeros((n,101),int),np.linspace(0,1,101),64,.01,risk='any',method='fixed_sequence')
|
||||
self.assertEqual(r['reject_all'],expected)
|
||||
self.assertEqual(r['threshold'],0.)
|
||||
def test_fixed_sequence_stops_without_restart(self):
|
||||
# Deliberately nonmonotone table: later passing tests must not be inspected.
|
||||
x=np.zeros((1000,3),int);x[:,1]=1
|
||||
r=calibrate(x,[.1,.5,.9],64,.01,risk='any',method='fixed_sequence')
|
||||
self.assertEqual(r['threshold'],.9)
|
||||
self.assertEqual([v['threshold'] for v in r['tested']],[.9,.5])
|
||||
def test_fixed_sequence_count_zero_boundary(self):
|
||||
n=math.ceil(math.log(.05)/math.log1p(-.01/64))
|
||||
for size,expected in ((n-1,True),(n,False)):
|
||||
r=calibrate(np.zeros((size,1),int),[.5],64,.01,method='fixed_sequence')
|
||||
self.assertEqual(r['reject_all'],expected)
|
||||
def test_fixed_sequence_support_endpoints(self):
|
||||
x=np.array([[0],[64]])
|
||||
self.assertTrue(calibrate(x,[.5],64,0,method='fixed_sequence')['reject_all'])
|
||||
self.assertFalse(calibrate(x,[.5],64,64,method='fixed_sequence')['reject_all'])
|
||||
def test_pool_cap_and_stability(self):
|
||||
p=np.zeros((5,5));p[0,0]=.9;p[4,4]=.8
|
||||
m,x=candidate_pool(p,.1,1);self.assertEqual(len(m),1);self.assertTrue(m[0][0,0])
|
||||
def test_metrics_one_to_one(self):
|
||||
g=np.zeros((9,9),bool);g[4,4]=1
|
||||
a=np.zeros_like(g);a[4,3]=1;b=np.zeros_like(g);b[4,5]=1
|
||||
r=image_stats([a,b],g);self.assertEqual((r['tp'],r['fp'],r['fn']),(1,1,0))
|
||||
def test_empty_image_conventions(self):
|
||||
r=aggregate([image_stats([],np.zeros((4,4),bool))])
|
||||
self.assertEqual(r['nIoU'],1.);self.assertIsNone(r['Pd']);self.assertEqual(r['FP_per_image'],0.)
|
||||
def test_path_matches_enumeration(self):
|
||||
nodes=[[1.,2.],[3.,0.],[.5,2.]];cost=[np.array([[0.,2.],[1.,0.]]),np.array([[0.,1.],[3.,0.]])]
|
||||
best,path=best_path(nodes,cost)
|
||||
scores=[(sum(nodes[t][v] for t,v in enumerate(p))-sum(cost[t][p[t],p[t+1]] for t in range(2)),p) for p in itertools.product(range(2),repeat=3)]
|
||||
self.assertAlmostEqual(best,max(s[0] for s in scores));self.assertEqual(len(path),3)
|
||||
def test_disconnected_path(self):
|
||||
self.assertEqual(best_path([[1.],[2.]],[np.array([[np.inf]])]),(-math.inf,[]))
|
||||
def test_binary_penalty_exactness(self):
|
||||
w=[1.,2.,3.];edges=[(0,1),(1,2)];lam=4.
|
||||
values=[(sum(w[i]*b[i] for i in range(3))-lam*sum(b[i]*b[j] for i,j in edges),b) for b in itertools.product([0,1],repeat=3)]
|
||||
value,b=max(values);self.assertFalse(any(b[i] and b[j] for i,j in edges));self.assertEqual(value,exact_mwis(w,edges)[0])
|
||||
@@ -0,0 +1,46 @@
|
||||
import unittest,tempfile,csv,json,subprocess,sys,os
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from irstd.data import sha,read_manifest
|
||||
|
||||
class PipelineTests(unittest.TestCase):
|
||||
def make_fixture(self,root):
|
||||
rows=[];pred=root/'pred';pred.mkdir()
|
||||
for i,split in enumerate(['train','dev','cal','cal','test']):
|
||||
name=f'image{i}';x=np.full((9,9),i,np.uint8);x[4,4]=200+i
|
||||
y=np.zeros((9,9),np.uint8);y[4,4]=255
|
||||
Image.fromarray(x).save(root/(name+'.png'));Image.fromarray(y).save(root/(name+'_mask.png'))
|
||||
prob=np.zeros((9,9),np.float32);prob[4,4]=.9;np.save(pred/(name+'.npy'),prob)
|
||||
rows.append(dict(id=name,image=name+'.png',mask=name+'_mask.png',split=split,group='g'+str(i)))
|
||||
manifest=root/'manifest.csv'
|
||||
with manifest.open('w',newline='') as f:
|
||||
w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
|
||||
(pred/'provenance.json').write_text(json.dumps(dict(checkpoint_sha256='synthetic_fixture_not_a_checkpoint',manifest_sha256=sha(manifest))))
|
||||
return manifest,pred,rows
|
||||
def run_script(self,name,args):
|
||||
return subprocess.run([sys.executable,'scripts/'+name]+list(map(str,args)),capture_output=True,text=True)
|
||||
def test_cpu_calibration_evaluation_and_tamper_guard(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root=Path(d);manifest,pred,_=self.make_fixture(root)
|
||||
cert=root/'cert.json';out=root/'eval.json'
|
||||
common=['--manifest',manifest,'--predictions',pred]
|
||||
r=self.run_script('evaluate.py',common+['--split','cal','--calibrate','--out',cert,'--alpha','.01'])
|
||||
self.assertEqual(r.returncode,0,r.stderr);self.assertTrue(json.loads(cert.read_text())['reject_all'])
|
||||
r=self.run_script('evaluate.py',common+['--certificate',cert,'--out',out])
|
||||
self.assertEqual(r.returncode,0,r.stderr);self.assertEqual(json.loads(out.read_text())['metrics']['Pd'],0.)
|
||||
np.save(pred/'image4.npy',np.ones((9,9),np.float32))
|
||||
r=self.run_script('evaluate.py',common+['--certificate',cert,'--out',root/'tampered.json'])
|
||||
self.assertNotEqual(r.returncode,0);self.assertIn('invalidated',r.stderr)
|
||||
def test_data_audit_cli(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root=Path(d);manifest,_,_=self.make_fixture(root);out=root/'audit.json'
|
||||
r=self.run_script('audit_data.py',['--manifest',manifest,'--out',out])
|
||||
self.assertEqual(r.returncode,0,r.stderr);report=json.loads(out.read_text())
|
||||
self.assertEqual(report['area_bins']['1-4'],5);self.assertEqual(report['splits']['cal'],2)
|
||||
def test_group_leakage_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root=Path(d);manifest,_,rows=self.make_fixture(root);rows[-1]['group']=rows[0]['group']
|
||||
with manifest.open('w',newline='') as f:
|
||||
w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
|
||||
with self.assertRaisesRegex(ValueError,'crosses splits'):read_manifest(manifest)
|
||||
@@ -0,0 +1,18 @@
|
||||
import unittest,importlib.util
|
||||
AVAILABLE=importlib.util.find_spec('torch') is not None
|
||||
@unittest.skipUnless(AVAILABLE,'PyTorch is unavailable; gradient/training tests NOT executed')
|
||||
class TorchTests(unittest.TestCase):
|
||||
def test_shapes_and_gradient(self):
|
||||
import torch
|
||||
from irstd.models import TinyUNet,segmentation_loss
|
||||
model=TinyUNet();x=torch.randn(1,1,17,19);y=torch.zeros_like(x);y[:,:,8,9]=1
|
||||
for name in ['bce','dice','bce_dice','focal','iou']:
|
||||
model.zero_grad();z=model(x);self.assertEqual(z.shape,x.shape)
|
||||
loss=segmentation_loss(z,y,name);loss.backward();self.assertTrue(torch.isfinite(loss))
|
||||
self.assertTrue(all(torch.isfinite(p.grad).all() for p in model.parameters()))
|
||||
def test_gate_gradients(self):
|
||||
import torch
|
||||
from irstd.models import gate_loss
|
||||
s=torch.tensor([0.,0.],requires_grad=True)
|
||||
loss,upper=gate_loss(s,torch.tensor([1.,0.]),torch.tensor([4.,1.]),1.,.1)
|
||||
loss.backward();self.assertLess(s.grad[0],0);self.assertGreater(s.grad[1],0);self.assertAlmostEqual(upper.item(),1.)
|
||||
Reference in new issue
Block a user