Skip to main content

mlt_core/frames/v01/
optimizer.rs

1use strum::{EnumCount as _, IntoEnumIterator as _};
2
3use crate::v01::sort::{reorder_features, spatial_sort_likely_to_help};
4use crate::v01::{
5    EncodeProperties as _, EncodedLayer01, GeometryEncoder, GeometryProfile, IdEncoder, IdProfile,
6    PropertyEncoder, PropertyProfile, SortStrategy, StagedLayer01, TileLayer01,
7};
8use crate::{MltError, MltResult};
9
10impl StagedLayer01 {
11    /// Encode using a specific [`StagedLayer01Encoder`], consuming `self` and producing [`EncodedLayer01`].
12    pub fn encode(self, encoder: StagedLayer01Encoder) -> MltResult<EncodedLayer01> {
13        let geometry = self.geometry.encode(encoder.geometry)?;
14
15        let id = match (encoder.id, self.id) {
16            (Some(id_enc), Some(id)) => id.encode(id_enc)?,
17            (None, Some(id)) => id.encode_auto()?.0,
18            _ => None,
19        };
20
21        let properties = self.properties.encode(encoder.properties)?;
22
23        Ok(EncodedLayer01 {
24            name: self.name,
25            extent: self.extent,
26            id,
27            geometry,
28            properties,
29            #[cfg(fuzzing)]
30            layer_order: vec![],
31        })
32    }
33
34    /// Profile-driven encode, consuming `self` and producing `(EncodedLayer01, StagedLayer01Encoder)`.
35    ///
36    /// Note: sort ordering is not applied here; call [`Tile01Encoder::encode`] before this method
37    /// if feature ordering matters.
38    pub fn encode_with_profile(
39        self,
40        profile: &Tag01Profile,
41    ) -> Result<(EncodedLayer01, StagedLayer01Encoder), MltError> {
42        let (geometry, geom_enc) = self.geometry.encode_with_profile(&profile.geometry)?;
43
44        let id_enc;
45        let id;
46        if let Some(parsed_id) = self.id {
47            let (enc_id, enc) = parsed_id.encode_with_profile(&profile.id)?;
48            id = enc_id;
49            id_enc = enc;
50        } else {
51            id = None;
52            id_enc = None;
53        }
54
55        let (properties, props_enc) = self.properties.encode_with_profile(&profile.properties)?;
56
57        let encoder = StagedLayer01Encoder {
58            id: id_enc,
59            properties: props_enc,
60            geometry: geom_enc,
61        };
62
63        Ok((
64            EncodedLayer01 {
65                name: self.name,
66                extent: self.extent,
67                id,
68                geometry,
69                properties,
70                #[cfg(fuzzing)]
71                layer_order: vec![],
72            },
73            encoder,
74        ))
75    }
76
77    /// Automatically select the best stream-level encoders by competitive trialing.
78    ///
79    /// This method does **not** attempt different sort strategies; call
80    /// [`Tile01Encoder::encode_auto`] instead when sort optimisation is also desired.
81    pub fn encode_auto(self) -> Result<(EncodedLayer01, StagedLayer01Encoder), MltError> {
82        let (geom_enc_result, geom_enc) = self.geometry.encode_auto()?;
83        let (id_enc_result, id_enc) = match self.id {
84            Some(parsed_id) => parsed_id.encode_auto()?,
85            None => (None, None),
86        };
87        let (encoded_properties, props_enc) = self.properties.encode_auto()?;
88
89        let stream_encoder = StagedLayer01Encoder {
90            id: id_enc,
91            properties: props_enc,
92            geometry: geom_enc,
93        };
94
95        let layer = EncodedLayer01 {
96            name: self.name,
97            extent: self.extent,
98            id: id_enc_result,
99            geometry: geom_enc_result,
100            properties: encoded_properties,
101            #[cfg(fuzzing)]
102            layer_order: vec![],
103        };
104
105        Ok((layer, stream_encoder))
106    }
107}
108
109/// Feature-count threshold above which the spatial trial is subject to the
110/// bounding-box pruning heuristic.
111const SORT_TRIAL_THRESHOLD: usize = 512;
112
113/// Candidate sort strategies evaluated during automatic competitive trialing.
114const TRIAL_STRATEGIES: [Option<SortStrategy>; 3] = [
115    None,
116    Some(SortStrategy::SpatialMorton),
117    Some(SortStrategy::Id),
118];
119
120/// Stream-level encoder configuration for a v01 layer.
121///
122/// Produced by any of the three optimisation paths (manual, automatic, or profile-driven)
123/// and consumed by [`StagedLayer01::encode`].  Sort ordering is handled separately by
124/// [`Tile01Encoder`] before this stage.
125#[derive(Debug, Clone)]
126pub struct StagedLayer01Encoder {
127    pub id: Option<IdEncoder>,
128    pub properties: Vec<PropertyEncoder>,
129    pub geometry: GeometryEncoder,
130}
131
132/// Entry-point encoder that converts a [`TileLayer01`] into a [`StagedLayer01`] and
133/// optionally reorders features according to a [`SortStrategy`].
134///
135/// For automatic sort-strategy selection, use [`Tile01Encoder::encode_auto`].
136#[derive(Debug, Clone, Default)]
137pub struct Tile01Encoder {
138    /// How to reorder features before columnar staging.  `None` preserves the
139    /// original input order.
140    pub sort_strategy: Option<SortStrategy>,
141}
142
143impl Tile01Encoder {
144    /// Reorder features in `data` according to the configured sort strategy
145    /// (no-op when `sort_strategy` is `None`), then convert to [`StagedLayer01`].
146    pub fn encode(&self, data: &mut TileLayer01) -> StagedLayer01 {
147        reorder_features(data, self.sort_strategy);
148        StagedLayer01::from(data.clone())
149    }
150
151    /// Automatically select the best sort strategy and stream-level encoders by
152    /// competitive trialing.
153    ///
154    /// # Algorithm
155    ///
156    /// 1. Build a candidate set: `[None, Some(Spatial(Morton)), Some(Id)]`.
157    ///    - When `N >= 512`, apply a bounding-box heuristic: if the vertex
158    ///      spread covers more than 80% of the tile extent on both axes,
159    ///      spatial sorting is unlikely to cluster features and is dropped from
160    ///      the candidates.
161    /// 2. For each candidate strategy:
162    ///    - Clone the source layer.
163    ///    - Apply `reorder_features` to the clone.
164    ///    - Convert the clone to `StagedLayer01` and run automatic
165    ///      stream-level optimisation on id, geometry, and properties.
166    ///    - Serialise the fully-encoded clone to a scratch buffer and record
167    ///      its byte count.
168    /// 3. Return the trial that produced the smallest byte count.
169    pub fn encode_auto(
170        source: &TileLayer01,
171    ) -> Result<(EncodedLayer01, StagedLayer01Encoder), MltError> {
172        struct TrialResult {
173            layer: EncodedLayer01,
174            stream_enc: StagedLayer01Encoder,
175            byte_count: usize,
176        }
177
178        let n = source.features.len();
179
180        let filtered: [Option<SortStrategy>; 2];
181        let candidates: &[Option<SortStrategy>] =
182            if n < SORT_TRIAL_THRESHOLD || spatial_sort_likely_to_help(source) {
183                &TRIAL_STRATEGIES
184            } else {
185                filtered = [None, Some(SortStrategy::Id)];
186                &filtered
187            };
188
189        let mut best: Option<TrialResult> = None;
190
191        for &strategy in candidates {
192            let mut trial_source = source.clone();
193            reorder_features(&mut trial_source, strategy);
194            let trial_staged = StagedLayer01::from(trial_source);
195
196            let (trial_layer, trial_stream_enc) = trial_staged.encode_auto()?;
197
198            let mut buf: Vec<u8> = Vec::new();
199            trial_layer.write_to(&mut buf)?;
200            let byte_count = buf.len();
201
202            if best.as_ref().is_none_or(|b| byte_count < b.byte_count) {
203                best = Some(TrialResult {
204                    layer: trial_layer,
205                    stream_enc: trial_stream_enc,
206                    byte_count,
207                });
208            }
209        }
210
211        // `candidates` always contains at least `None`, so `best` is always `Some`.
212        let winner = best.unwrap();
213        Ok((winner.layer, winner.stream_enc))
214    }
215}
216
217/// Map `Option<SortStrategy>` to a vote-array index.
218fn sort_strategy_index(s: Option<SortStrategy>) -> usize {
219    match s {
220        None => 0,
221        Some(s) => {
222            1 + SortStrategy::iter()
223                .position(|v| v == s)
224                .expect("variant must be present in iter()")
225        }
226    }
227}
228
229/// Profile for a v01 layer, built by running automatic optimisation over a
230/// representative sample of tiles and capturing the chosen encoders.
231#[derive(Debug, Clone)]
232pub struct Tag01Profile {
233    strategy_votes: [u32; SortStrategy::COUNT + 1],
234    pub id: IdProfile,
235    pub properties: PropertyProfile,
236    pub geometry: GeometryProfile,
237}
238
239impl Tag01Profile {
240    #[must_use]
241    pub fn new(
242        sort_strategy: Option<SortStrategy>,
243        id: IdProfile,
244        properties: PropertyProfile,
245        geometry: GeometryProfile,
246    ) -> Self {
247        let mut strategy_votes = [0u32; SortStrategy::COUNT + 1];
248        strategy_votes[sort_strategy_index(sort_strategy)] = 1;
249        Self {
250            strategy_votes,
251            id,
252            properties,
253            geometry,
254        }
255    }
256
257    #[must_use]
258    pub fn sort_strategy(&self) -> Option<SortStrategy> {
259        let winner_idx = self
260            .strategy_votes
261            .iter()
262            .enumerate()
263            .fold(0usize, |best, (i, &v)| {
264                if v > self.strategy_votes[best] {
265                    i
266                } else {
267                    best
268                }
269            });
270        if winner_idx == 0 {
271            None
272        } else {
273            SortStrategy::iter().nth(winner_idx - 1)
274        }
275    }
276
277    pub fn set_sort_strategy(&mut self, strategy: Option<SortStrategy>) {
278        self.strategy_votes = [0u32; SortStrategy::COUNT + 1];
279        self.strategy_votes[sort_strategy_index(strategy)] = 1;
280    }
281
282    #[must_use]
283    pub fn merge(self, other: &Self) -> Self {
284        let mut votes = self.strategy_votes;
285        for (v, &o) in votes.iter_mut().zip(other.strategy_votes.iter()) {
286            *v = v.saturating_add(o);
287        }
288        Self {
289            strategy_votes: votes,
290            id: self.id.merge(&other.id),
291            properties: self.properties.merge(&other.properties),
292            geometry: self.geometry.merge(&other.geometry),
293        }
294    }
295}