Skip to content

Commit bbaf01b

Browse files
evanlinjinclaude
andcommitted
feat(metrics): add Changeless::restrict for tuple-combinator ergonomics
Adds a thin constructor for the "restrict any metric to changeless solutions" pattern. The previous workaround was the verbose tuple combinator `((metric, 1.0), (Changeless { target, change_policy }, 0.0))`, which is how the new method is implemented: Changeless { target, change_policy } .restrict(LowestFee { target, long_term_feerate, change_policy }); For metrics like `LowestFee` whose bounds happen not to differ per-regime on changeless selections, this is equivalent in BnB round counts to a dedicated `Changeless<Lowest>Fee` metric — see the `benches/changeless_dedicated_vs_tuple` data. For `Waste`-shape metrics with genuinely cross-regime bounds, the dedicated `ChangelessWaste` is much tighter; the rustdoc on `restrict` documents this trade-off. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8a984e3 commit bbaf01b

1 file changed

Lines changed: 30 additions & 0 deletions

File tree

src/metrics/changeless.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,36 @@ pub struct Changeless {
1010
pub change_policy: ChangePolicy,
1111
}
1212

13+
impl Changeless {
14+
/// Restrict another metric to changeless solutions only.
15+
///
16+
/// Returns the tuple combinator `((metric, 1.0), (self, 0.0))` — `metric` does the actual
17+
/// scoring; `self` (with weight `0.0`) contributes nothing to the score but its `score`
18+
/// returning `None` whenever the selection has change is what filters out non-changeless
19+
/// solutions. The `bound` likewise propagates through both factors: as soon as
20+
/// `Changeless::bound` returns `None` (no changeless descendant possible per
21+
/// `change_lower_bound`), the whole branch is pruned.
22+
///
23+
/// ```
24+
/// use bdk_coin_select::metrics::{Changeless, LowestFee};
25+
/// # use bdk_coin_select::{Target, ChangePolicy, FeeRate, DrainWeights, TargetOutputs};
26+
/// # let target = Target { fee: Default::default(), outputs: TargetOutputs::fund_outputs([]) };
27+
/// # let change_policy = ChangePolicy::min_value(DrainWeights::NONE, 0);
28+
/// # let long_term_feerate = FeeRate::ZERO;
29+
/// let metric = Changeless { target, change_policy }
30+
/// .restrict(LowestFee { target, long_term_feerate, change_policy });
31+
/// ```
32+
///
33+
/// Note that for `LowestFee` specifically this is equivalent in `BnB` round counts to a
34+
/// dedicated changeless-LowestFee metric (the cross-regime branch in `LowestFee::bound`
35+
/// does not fire on changeless selections). For metrics with bounds that *do* differ
36+
/// per-regime — like the `Waste` metric — a dedicated `ChangelessWaste` is significantly
37+
/// tighter.
38+
pub fn restrict<M: BnbMetric>(self, metric: M) -> ((M, f32), (Self, f32)) {
39+
((metric, 1.0), (self, 0.0))
40+
}
41+
}
42+
1343
impl BnbMetric for Changeless {
1444
fn score(&mut self, cs: &CoinSelector<'_>) -> Option<Ordf32> {
1545
if cs.is_target_met(self.target)

0 commit comments

Comments
 (0)