molgfx-core 0.2.1

The semantic scene graph: columnar tables, GPU record layouts, the borrowed coordinate seam.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
//! Caller-supplied molecular interactions and their deterministic glyph style.
//!
//! Detection, geometric classification and trajectory aggregation belong to
//! `molframe` or the caller. The render engine validates those facts and maps
//! them to a stable visual vocabulary; it never infers chemistry here.

#[cfg(test)]
#[path = "interaction_tests.rs"]
mod tests;

use crate::{CoreError, EntityRef, StructureHandle};
use molgfx_math::{Rgba8, Vec3};
use std::sync::Arc;

/// Chemically meaningful interaction class supplied by the caller.
#[repr(u32)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InteractionKind {
    /// Donor-to-acceptor hydrogen bond.
    HydrogenBond,
    /// Oppositely charged group contact.
    SaltBridge,
    /// Aromatic ring stacking interaction.
    PiStacking,
    /// Non-polar contact.
    Hydrophobic,
    /// Metal-to-ligand coordination.
    MetalCoordination,
}

/// Direction carried by an interaction, when its source establishes one.
#[repr(u32)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum InteractionDirection {
    /// The interaction has no directional meaning.
    #[default]
    Undirected,
    /// The glyph points from the first anchor to the second.
    Forward,
    /// The glyph points from the second anchor to the first.
    Reverse,
}

/// Repeating screen-space mark used for an interaction line.
#[repr(u32)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InteractionPattern {
    /// Continuous analytic line.
    Solid,
    /// Repeating line segments.
    Dashes,
    /// Repeating round marks.
    Dots,
    /// Continuous sinusoidal spring guide.
    Spring,
}

/// One already-resolved world-space interaction endpoint.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct InteractionAnchor {
    position: Vec3,
    entity: Option<EntityRef>,
}

impl InteractionAnchor {
    /// Creates a free world-space anchor such as a ring or group centroid.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] for non-finite coordinates.
    pub fn world(position: Vec3) -> Result<Self, CoreError> {
        Self::new(position, None)
    }

    /// Creates an anchor associated with an atom or another scene entity.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] for non-finite coordinates.
    pub fn entity(position: Vec3, entity: EntityRef) -> Result<Self, CoreError> {
        Self::new(position, Some(entity))
    }

    fn new(position: Vec3, entity: Option<EntityRef>) -> Result<Self, CoreError> {
        if !position.is_finite() {
            return Err(CoreError::InvalidInteraction {
                reason: "anchor position must be finite",
            });
        }
        Ok(Self { position, entity })
    }

    /// Caller-resolved world-space position in Ångström.
    #[must_use]
    pub const fn position(self) -> Vec3 {
        self.position
    }

    /// Optional source entity represented by this anchor.
    #[must_use]
    pub const fn source_entity(self) -> Option<EntityRef> {
        self.entity
    }
}

/// Caller-computed geometry retained for inspection and labels.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct InteractionGeometry {
    distance_angstrom: f32,
    angle_degrees: Option<f32>,
}

impl InteractionGeometry {
    /// Stores a positive distance and optional angle in `[0, 180]`.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] for malformed values.
    pub fn new(distance_angstrom: f32, angle_degrees: Option<f32>) -> Result<Self, CoreError> {
        if !distance_angstrom.is_finite() || distance_angstrom <= 0.0 {
            return Err(CoreError::InvalidInteraction {
                reason: "distance must be finite and positive",
            });
        }
        if angle_degrees.is_some_and(|angle| !angle.is_finite() || !(0.0..=180.0).contains(&angle))
        {
            return Err(CoreError::InvalidInteraction {
                reason: "angle must be finite and within 0 to 180 degrees",
            });
        }
        Ok(Self {
            distance_angstrom,
            angle_degrees,
        })
    }

    /// Source-reported distance in Ångström.
    #[must_use]
    pub const fn distance_angstrom(self) -> f32 {
        self.distance_angstrom
    }

    /// Optional source-reported angle in degrees.
    #[must_use]
    pub const fn angle_degrees(self) -> Option<f32> {
        self.angle_degrees
    }
}

/// Fully resolved glyph presentation, derivable from the scientific inputs.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct InteractionStyle {
    /// Interaction-class color.
    pub color: Rgba8,
    /// Screen-space mark vocabulary.
    pub pattern: InteractionPattern,
    /// Pixel-stable line width.
    pub width_pixels: f32,
    /// Final alpha before weighted transparency.
    pub opacity: f32,
    /// Repetition period in pixels; ignored by solid lines.
    pub period_pixels: f32,
    /// Fraction of each period occupied by a mark.
    pub duty_cycle: f32,
    /// Optional deterministic presentation phase speed in pixels per frame.
    /// Zero keeps the scientific glyph static.
    pub phase_speed_pixels_per_frame: f32,
}

/// One scientific interaction edge owned by the scene.
#[derive(Clone, PartialEq, Debug)]
pub struct InteractionEdge {
    owner: StructureHandle,
    start: InteractionAnchor,
    end: InteractionAnchor,
    kind: InteractionKind,
    direction: InteractionDirection,
    geometry: InteractionGeometry,
    occupancy: Option<f32>,
    normalized_strength: Option<f32>,
    phase_speed_pixels_per_frame: f32,
    persistence_age_frames: u32,
    persistence_half_life_frames: f32,
    provenance: Arc<str>,
    visible: bool,
}

impl InteractionEdge {
    /// Creates a visible edge from caller-computed facts.
    ///
    /// `provenance` identifies the computation or source dataset; it must not
    /// be empty because the renderer must not present an unexplained claim.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] for coincident anchors or
    /// empty provenance.
    pub fn new(
        owner: StructureHandle,
        start: InteractionAnchor,
        end: InteractionAnchor,
        kind: InteractionKind,
        geometry: InteractionGeometry,
        provenance: impl Into<Arc<str>>,
    ) -> Result<Self, CoreError> {
        let provenance = provenance.into();
        if provenance.trim().is_empty() {
            return Err(CoreError::InvalidInteraction {
                reason: "provenance must not be empty",
            });
        }
        if start.position().distance_squared(end.position()) <= f32::EPSILON {
            return Err(CoreError::InvalidInteraction {
                reason: "interaction anchors must not coincide",
            });
        }
        Ok(Self {
            owner,
            start,
            end,
            kind,
            direction: InteractionDirection::Undirected,
            geometry,
            occupancy: None,
            normalized_strength: None,
            phase_speed_pixels_per_frame: 0.0,
            persistence_age_frames: 0,
            persistence_half_life_frames: 0.0,
            provenance,
            visible: true,
        })
    }

    /// Sets source direction without changing endpoint identity.
    #[must_use]
    pub const fn with_direction(mut self, direction: InteractionDirection) -> Self {
        self.direction = direction;
        self
    }

    /// Sets optional occupancy in `[0, 1]`; occupancy maps only to opacity.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] outside the normalized range.
    pub fn with_occupancy(mut self, occupancy: f32) -> Result<Self, CoreError> {
        self.occupancy = Some(normalized(occupancy, "occupancy must be within 0 to 1")?);
        Ok(self)
    }

    /// Sets optional normalized strength in `[0, 1]`; strength maps only to
    /// line width.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] outside the normalized range.
    pub fn with_normalized_strength(mut self, strength: f32) -> Result<Self, CoreError> {
        self.normalized_strength = Some(normalized(
            strength,
            "normalized strength must be within 0 to 1",
        )?);
        Ok(self)
    }

    /// Adds an optional deterministic presentation phase speed in pixels per
    /// frame. It changes only the animated glyph phase, never the source
    /// occupancy or interaction geometry.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] outside `[0, 64]` pixels per
    /// frame.
    pub fn with_phase_speed(mut self, speed: f32) -> Result<Self, CoreError> {
        if !speed.is_finite() || !(0.0..=64.0).contains(&speed) {
            return Err(CoreError::InvalidInteraction {
                reason: "phase speed must be finite and within 0 to 64 pixels per frame",
            });
        }
        self.phase_speed_pixels_per_frame = speed;
        Ok(self)
    }

    /// Supplies caller-computed visual persistence for an interaction that was
    /// last observed some frames ago. A zero half-life disables decay. This is
    /// presentation state only: it never changes the stored occupancy or
    /// claims that the interaction remains scientifically present.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::InvalidInteraction`] for a non-finite or negative
    /// half-life, or for a half-life above one million frames.
    pub fn with_persistence(
        mut self,
        age_frames: u32,
        half_life_frames: f32,
    ) -> Result<Self, CoreError> {
        if !half_life_frames.is_finite() || !(0.0..=1_000_000.0).contains(&half_life_frames) {
            return Err(CoreError::InvalidInteraction {
                reason: "persistence half-life must be finite and within 0 to 1000000 frames",
            });
        }
        self.persistence_age_frames = age_frames;
        self.persistence_half_life_frames = half_life_frames;
        Ok(self)
    }

    /// Replaces the caller-computed visual age without changing its half-life.
    pub const fn set_persistence_age(&mut self, age_frames: u32) {
        self.persistence_age_frames = age_frames;
    }

    /// Changes object-level visibility without changing scientific inputs.
    pub const fn set_visible(&mut self, visible: bool) {
        self.visible = visible;
    }

    /// Structure used for picking and lifecycle ownership.
    #[must_use]
    pub const fn owner(&self) -> StructureHandle {
        self.owner
    }

    /// First source anchor.
    #[must_use]
    pub const fn start(&self) -> InteractionAnchor {
        self.start
    }

    /// Second source anchor.
    #[must_use]
    pub const fn end(&self) -> InteractionAnchor {
        self.end
    }

    /// Scientific interaction class.
    #[must_use]
    pub const fn kind(&self) -> InteractionKind {
        self.kind
    }

    /// Caller-supplied direction.
    #[must_use]
    pub const fn direction(&self) -> InteractionDirection {
        self.direction
    }

    /// Caller-computed geometry.
    #[must_use]
    pub const fn geometry(&self) -> InteractionGeometry {
        self.geometry
    }

    /// Optional source occupancy.
    #[must_use]
    pub const fn occupancy(&self) -> Option<f32> {
        self.occupancy
    }

    /// Optional normalized source strength.
    #[must_use]
    pub const fn normalized_strength(&self) -> Option<f32> {
        self.normalized_strength
    }

    /// Optional deterministic visual phase speed.
    #[must_use]
    pub const fn phase_speed_pixels_per_frame(&self) -> f32 {
        self.phase_speed_pixels_per_frame
    }

    /// Number of frames since the caller last observed this interaction.
    #[must_use]
    pub const fn persistence_age_frames(&self) -> u32 {
        self.persistence_age_frames
    }

    /// Presentation half-life in frames; zero means no visual decay.
    #[must_use]
    pub const fn persistence_half_life_frames(&self) -> f32 {
        self.persistence_half_life_frames
    }

    /// Source computation or dataset identifier.
    #[must_use]
    pub fn provenance(&self) -> &str {
        &self.provenance
    }

    /// Whether this scene object participates in rendering.
    #[must_use]
    pub const fn visible(&self) -> bool {
        self.visible
    }

    /// Deterministically maps the scientific record to its default glyph.
    ///
    /// Occupancy maps linearly to opacity (`0.35 + 0.65 × occupancy`) and
    /// normalized strength maps linearly to width (`base + 1.5 × strength`).
    /// Both mappings are independently invertible; absent values use the
    /// class default rather than inventing evidence.
    #[must_use]
    pub fn resolved_style(&self) -> InteractionStyle {
        let (color, pattern, base_width, period, duty) = match self.kind {
            InteractionKind::HydrogenBond => (
                Rgba8::opaque(70, 180, 255),
                InteractionPattern::Dashes,
                1.6,
                9.0,
                0.52,
            ),
            InteractionKind::SaltBridge => (
                Rgba8::opaque(238, 90, 210),
                InteractionPattern::Dashes,
                2.0,
                12.0,
                0.68,
            ),
            InteractionKind::PiStacking => (
                Rgba8::opaque(255, 166, 54),
                InteractionPattern::Dots,
                1.9,
                8.0,
                0.34,
            ),
            InteractionKind::Hydrophobic => (
                Rgba8::opaque(230, 205, 72),
                InteractionPattern::Dots,
                1.5,
                7.0,
                0.28,
            ),
            InteractionKind::MetalCoordination => (
                Rgba8::opaque(80, 225, 205),
                InteractionPattern::Solid,
                2.1,
                1.0,
                1.0,
            ),
        };
        let base_opacity = self.occupancy.map_or(0.85, |value| 0.35 + value * 0.65);
        let persistence = if self.persistence_half_life_frames > 0.0 {
            2.0_f32.powf(
                -exact_u32_to_f32(self.persistence_age_frames) / self.persistence_half_life_frames,
            )
        } else {
            1.0
        };
        InteractionStyle {
            color,
            pattern,
            width_pixels: base_width
                + self
                    .normalized_strength
                    .into_iter()
                    .fold(0.0, |_, value| value)
                    * 1.5,
            opacity: base_opacity * persistence,
            period_pixels: period,
            duty_cycle: duty,
            phase_speed_pixels_per_frame: self.phase_speed_pixels_per_frame,
        }
    }
}

fn exact_u32_to_f32(value: u32) -> f32 {
    let high = u16::try_from(value >> 16)
        .into_iter()
        .fold(u16::MAX, |_, part| part);
    let low = u16::try_from(value & u32::from(u16::MAX))
        .into_iter()
        .fold(u16::MAX, |_, part| part);
    f32::from(high) * 65_536.0 + f32::from(low)
}

fn normalized(value: f32, reason: &'static str) -> Result<f32, CoreError> {
    if value.is_finite() && (0.0..=1.0).contains(&value) {
        Ok(value)
    } else {
        Err(CoreError::InvalidInteraction { reason })
    }
}