@@ -225,7 +225,8 @@ pub struct BiasCreateConfig {
225225 pub target_negative_retention : Option < f32 > ,
226226 pub positive_fscale : Option < u64 > ,
227227 pub negative_fscale : Option < u64 > ,
228- pub curve_power : f32 ,
228+ pub deadzone_pos : f32 ,
229+ pub deadzone_neg : f32 ,
229230}
230231
231232#[ derive( Debug , Clone , Copy ) ]
@@ -251,7 +252,8 @@ pub struct HashBiasTable {
251252 pub negative_fscale : u64 ,
252253 pub k_pos : f32 ,
253254 pub k_neg : f32 ,
254- pub curve_power : f32 ,
255+ pub deadzone_pos : f32 ,
256+ pub deadzone_neg : f32 ,
255257}
256258
257259#[ derive( Debug , Clone , Copy ) ]
@@ -365,7 +367,8 @@ impl HashBiasTable {
365367 config. target_negative_retention ,
366368 config. positive_fscale . unwrap_or ( 0 ) ,
367369 config. negative_fscale . unwrap_or ( 0 ) ,
368- config. curve_power ,
370+ config. deadzone_pos ,
371+ config. deadzone_neg ,
369372 ) ?;
370373
371374 if let Some ( ref pb) = progress {
@@ -384,16 +387,20 @@ impl HashBiasTable {
384387 target_negative_retention : Option < f32 > ,
385388 positive_fscale : u64 ,
386389 negative_fscale : u64 ,
387- curve_power : f32 ,
390+ deadzone_pos : f32 ,
391+ deadzone_neg : f32 ,
388392 ) -> Result < Self > {
389393 validate_cms_compatibility ( positive, negative) ?;
390394
391395 if !alpha. is_finite ( ) || alpha <= 0.0 {
392396 anyhow:: bail!( "alpha must be finite and > 0, got {}" , alpha) ;
393397 }
394398
395- if !curve_power. is_finite ( ) || curve_power <= 0.0 {
396- anyhow:: bail!( "curve_power must be finite and > 0, got {}" , curve_power) ;
399+ if !deadzone_pos. is_finite ( ) || !( 0.0 ..1.0 ) . contains ( & deadzone_pos) {
400+ anyhow:: bail!( "deadzone_pos must be finite and in [0.0, 1.0), got {}" , deadzone_pos) ;
401+ }
402+ if !deadzone_neg. is_finite ( ) || !( 0.0 ..1.0 ) . contains ( & deadzone_neg) {
403+ anyhow:: bail!( "deadzone_neg must be finite and in [0.0, 1.0), got {}" , deadzone_neg) ;
397404 }
398405
399406 if let Some ( target) = target_positive_retention {
@@ -475,7 +482,8 @@ impl HashBiasTable {
475482 negative_fscale,
476483 k_pos : 0.0 ,
477484 k_neg : 0.0 ,
478- curve_power,
485+ deadzone_pos,
486+ deadzone_neg,
479487 } ;
480488
481489 // Auto-derive k_pos and k_neg when soft filter is active
@@ -680,14 +688,20 @@ impl HashBiasTable {
680688
681689 /// Compute the effective fscale using piecewise sigmoid centered at 0.
682690 /// w >= 0 uses k_pos, w < 0 uses k_neg. At w=0: s=0.5 → geometric mean (midpoint anchor).
683- /// When curve_power > 1.0, a power transform flattens the sigmoid 's middle region,
684- /// so only strongly biased k-mers get preferential/penalized treatment .
691+ /// deadzone_pos/deadzone_neg define the fraction of each side 's weight range that stays
692+ /// flat (no effect) before the sigmoid transition kicks in .
685693 #[ inline]
686694 fn effective_fscale ( & self , w : i8 ) -> f64 {
687695 let k = if w >= 0 { self . k_pos } else { self . k_neg } ;
696+ let deadzone = if w >= 0 { self . deadzone_pos } else { self . deadzone_neg } as f64 ;
688697 let w_norm = w as f64 / 127.0 ;
689- let w_curved = w_norm. signum ( ) * w_norm. abs ( ) . powf ( self . curve_power as f64 ) ;
690- let x = k as f64 * w_curved * 127.0 ;
698+ let abs_w = w_norm. abs ( ) ;
699+ let remapped = if abs_w <= deadzone {
700+ 0.0
701+ } else {
702+ ( abs_w - deadzone) / ( 1.0 - deadzone)
703+ } ;
704+ let x = k as f64 * w_norm. signum ( ) * remapped * 127.0 ;
691705 let s = 1.0 / ( 1.0 + ( -x) . exp ( ) ) ;
692706 let log_pos = ( self . positive_fscale as f64 ) . ln ( ) ;
693707 let log_neg = ( self . negative_fscale as f64 ) . ln ( ) ;
@@ -748,7 +762,8 @@ impl HashBiasTable {
748762 file. write_all ( & self . negative_fscale . to_le_bytes ( ) ) ?;
749763 file. write_all ( & self . k_pos . to_le_bytes ( ) ) ?;
750764 file. write_all ( & self . k_neg . to_le_bytes ( ) ) ?;
751- file. write_all ( & self . curve_power . to_le_bytes ( ) ) ?;
765+ file. write_all ( & self . deadzone_pos . to_le_bytes ( ) ) ?;
766+ file. write_all ( & self . deadzone_neg . to_le_bytes ( ) ) ?;
752767
753768 for & seed in & self . seeds {
754769 file. write_all ( & seed. to_le_bytes ( ) ) ?;
@@ -826,7 +841,10 @@ impl HashBiasTable {
826841 let k_neg = f32:: from_le_bytes ( buf4) ;
827842
828843 file. read_exact ( & mut buf4) ?;
829- let curve_power = f32:: from_le_bytes ( buf4) ;
844+ let deadzone_pos = f32:: from_le_bytes ( buf4) ;
845+
846+ file. read_exact ( & mut buf4) ?;
847+ let deadzone_neg = f32:: from_le_bytes ( buf4) ;
830848
831849 let mut seeds = Vec :: with_capacity ( depth) ;
832850 for _ in 0 ..depth {
@@ -867,12 +885,13 @@ impl HashBiasTable {
867885 negative_fscale,
868886 k_pos,
869887 k_neg,
870- curve_power,
888+ deadzone_pos,
889+ deadzone_neg,
871890 } )
872891 }
873892
874893 pub fn to_bytes ( & self ) -> Vec < u8 > {
875- let header_size = 4 + 4 + 1 + 8 + 4 + 1 + 4 + 1 + 4 + 4 + 8 + 8 + 4 + 4 + 4 ; // 63 bytes
894+ let header_size = 4 + 4 + 1 + 8 + 4 + 1 + 4 + 1 + 4 + 4 + 8 + 8 + 4 + 4 + 4 + 4 ; // 67 bytes
876895 let seeds_size = self . config . depth * 8 ;
877896 let weights_size = self . config . width * self . config . depth ;
878897 let total_size = header_size + seeds_size + weights_size;
@@ -892,7 +911,8 @@ impl HashBiasTable {
892911 out. extend_from_slice ( & self . negative_fscale . to_le_bytes ( ) ) ;
893912 out. extend_from_slice ( & self . k_pos . to_le_bytes ( ) ) ;
894913 out. extend_from_slice ( & self . k_neg . to_le_bytes ( ) ) ;
895- out. extend_from_slice ( & self . curve_power . to_le_bytes ( ) ) ;
914+ out. extend_from_slice ( & self . deadzone_pos . to_le_bytes ( ) ) ;
915+ out. extend_from_slice ( & self . deadzone_neg . to_le_bytes ( ) ) ;
896916
897917 for & seed in & self . seeds {
898918 out. extend_from_slice ( & seed. to_le_bytes ( ) ) ;
@@ -938,9 +958,10 @@ impl HashBiasTable {
938958 let negative_fscale = u64:: from_le_bytes ( data[ 43 ..51 ] . try_into ( ) . unwrap ( ) ) ;
939959 let k_pos = f32:: from_le_bytes ( data[ 51 ..55 ] . try_into ( ) . unwrap ( ) ) ;
940960 let k_neg = f32:: from_le_bytes ( data[ 55 ..59 ] . try_into ( ) . unwrap ( ) ) ;
941- let curve_power = f32:: from_le_bytes ( data[ 59 ..63 ] . try_into ( ) . unwrap ( ) ) ;
961+ let deadzone_pos = f32:: from_le_bytes ( data[ 59 ..63 ] . try_into ( ) . unwrap ( ) ) ;
962+ let deadzone_neg = f32:: from_le_bytes ( data[ 63 ..67 ] . try_into ( ) . unwrap ( ) ) ;
942963
943- let seeds_start = 63 ;
964+ let seeds_start = 67 ;
944965 let seeds_end = seeds_start + depth * 8 ;
945966 let weights_start = seeds_end;
946967 let weights_end = weights_start + width * depth;
@@ -992,7 +1013,8 @@ impl HashBiasTable {
9921013 negative_fscale,
9931014 k_pos,
9941015 k_neg,
995- curve_power,
1016+ deadzone_pos,
1017+ deadzone_neg,
9961018 } )
9971019 }
9981020
@@ -1087,7 +1109,8 @@ impl HashBiasTable {
10871109 if self . is_soft_filter ( ) {
10881110 eprintln ! ( " k_pos: {:.4}" , self . k_pos) ;
10891111 eprintln ! ( " k_neg: {:.4}" , self . k_neg) ;
1090- eprintln ! ( " curve_power: {:.2}" , self . curve_power) ;
1112+ eprintln ! ( " deadzone_pos: {:.2}" , self . deadzone_pos) ;
1113+ eprintln ! ( " deadzone_neg: {:.2}" , self . deadzone_neg) ;
10911114
10921115 eprintln ! ( " reference points:" ) ;
10931116 for p in self . soft_filter_reference_points ( ) {
@@ -1388,7 +1411,8 @@ impl PartialEq for HashBiasTable {
13881411 && self . negative_fscale == other. negative_fscale
13891412 && self . k_pos == other. k_pos
13901413 && self . k_neg == other. k_neg
1391- && self . curve_power == other. curve_power
1414+ && self . deadzone_pos == other. deadzone_pos
1415+ && self . deadzone_neg == other. deadzone_neg
13921416 && self . seeds == other. seeds
13931417 && self . weights == other. weights
13941418 }
@@ -1499,7 +1523,7 @@ mod tests {
14991523 )
15001524 . unwrap ( ) ;
15011525
1502- let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 1 .0) . unwrap ( ) ;
1526+ let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 0.0 , 0 .0) . unwrap ( ) ;
15031527 assert ! ( table. threshold >= -127 ) ;
15041528 }
15051529
@@ -1530,7 +1554,7 @@ mod tests {
15301554 )
15311555 . unwrap ( ) ;
15321556
1533- let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 1 .0) . unwrap ( ) ;
1557+ let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 0.0 , 0 .0) . unwrap ( ) ;
15341558
15351559 let output = NamedTempFile :: new ( ) . unwrap ( ) ;
15361560 table. save ( output. path ( ) ) . unwrap ( ) ;
@@ -1568,7 +1592,7 @@ mod tests {
15681592 )
15691593 . unwrap ( ) ;
15701594
1571- let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 1 .0) . unwrap ( ) ;
1595+ let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 0.0 , 0 .0) . unwrap ( ) ;
15721596
15731597 let bytes = table. to_bytes ( ) ;
15741598 let loaded = HashBiasTable :: from_bytes ( & bytes) . unwrap ( ) ;
@@ -1603,7 +1627,7 @@ mod tests {
16031627 )
16041628 . unwrap ( ) ;
16051629
1606- let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 1 .0) . unwrap ( ) ;
1630+ let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 0.0 , 0 .0) . unwrap ( ) ;
16071631
16081632 let mut passed = 0 ;
16091633 let mut failed = 0 ;
@@ -1651,7 +1675,7 @@ mod tests {
16511675 )
16521676 . unwrap ( ) ;
16531677
1654- let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 1 .0) . unwrap ( ) ;
1678+ let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 0.0 , 0 .0) . unwrap ( ) ;
16551679 assert ! ( table. threshold >= -127 ) ;
16561680 assert ! ( table. fold_enrichment( ) >= 1.0 ) ;
16571681 }
@@ -1673,7 +1697,8 @@ mod tests {
16731697 target_negative_retention : None ,
16741698 positive_fscale : None ,
16751699 negative_fscale : None ,
1676- curve_power : 1.0 ,
1700+ deadzone_pos : 0.0 ,
1701+ deadzone_neg : 0.0 ,
16771702 } ;
16781703
16791704 let table = HashBiasTable :: create ( & [ pos. path ( ) ] , & [ neg. path ( ) ] , & config, None ) . unwrap ( ) ;
@@ -1716,7 +1741,7 @@ mod tests {
17161741 . unwrap ( ) ;
17171742
17181743 // Hard cutoff mode (positive_fscale=0, negative_fscale=0)
1719- let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 1 .0) . unwrap ( ) ;
1744+ let table = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 0 , 0.0 , 0 .0) . unwrap ( ) ;
17201745 assert ! ( !table. is_soft_filter( ) ) ;
17211746
17221747 // Verify passes_filter matches weight >= threshold for many hashes
@@ -1761,7 +1786,7 @@ mod tests {
17611786 . unwrap ( ) ;
17621787
17631788 let table =
1764- HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 1 , 1000 , 1 .0) . unwrap ( ) ;
1789+ HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 1 , 1000 , 0.0 , 0 .0) . unwrap ( ) ;
17651790 assert ! ( table. is_soft_filter( ) ) ;
17661791
17671792 // Same hash always gives the same result
@@ -1800,7 +1825,7 @@ mod tests {
18001825 . unwrap ( ) ;
18011826
18021827 let table =
1803- HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 10 , 5000 , 1 .0) . unwrap ( ) ;
1828+ HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 10 , 5000 , 0.0 , 0 .0) . unwrap ( ) ;
18041829 assert ! ( table. is_soft_filter( ) ) ;
18051830
18061831 let bytes = table. to_bytes ( ) ;
@@ -1836,7 +1861,7 @@ mod tests {
18361861 . unwrap ( ) ;
18371862
18381863 let table =
1839- HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 10_000 , 1 .0) . unwrap ( ) ;
1864+ HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 10_000 , 0.0 , 0 .0) . unwrap ( ) ;
18401865 assert ! ( table. is_soft_filter( ) ) ;
18411866
18421867 let expected = ( 100.0_f64 * 10_000.0_f64 ) . sqrt ( ) ;
@@ -1872,23 +1897,23 @@ mod tests {
18721897 . unwrap ( ) ;
18731898
18741899 // positive_fscale < global fscale → error
1875- let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 50 , 1000 , 1 .0) ;
1900+ let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 50 , 1000 , 0.0 , 0 .0) ;
18761901 assert ! ( result. is_err( ) ) ;
18771902
18781903 // positive_fscale without negative_fscale → error
1879- let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 0 , 1 .0) ;
1904+ let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 0 , 0.0 , 0 .0) ;
18801905 assert ! ( result. is_err( ) ) ;
18811906
18821907 // negative_fscale without positive_fscale → error
1883- let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 1000 , 1 .0) ;
1908+ let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 0 , 1000 , 0.0 , 0 .0) ;
18841909 assert ! ( result. is_err( ) ) ;
18851910
18861911 // negative_fscale <= positive_fscale → error
1887- let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 100 , 1 .0) ;
1912+ let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 100 , 0.0 , 0 .0) ;
18881913 assert ! ( result. is_err( ) ) ;
18891914
18901915 // Valid soft filter
1891- let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 5000 , 1 .0) ;
1916+ let result = HashBiasTable :: build ( & pos_raw, & neg_raw, 1.0 , None , None , 100 , 5000 , 0.0 , 0 .0) ;
18921917 assert ! ( result. is_ok( ) ) ;
18931918 }
18941919}
0 commit comments