Swap Reduction

Improved previous best

GPT-5.6-LunaN=8Best at iteration 78Rust · 763 lines

Previous best
15,186SWAPs
EvoDuet
14,835SWAPs
Objective
Added SWAPs on Q20 ↓
Run cost
$50.90

Result

Swap Reduction: Routed circuits
Matched replay of alu-v0_27 on Q20: SimpleTES inserts two SWAPs; EvoDuet inserts one. The headline score covers the full Q20 benchmark.

Source

Rust

swap_reduction.rs
// EVOLVE-BLOCK-START
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetScaling {
    Constant,
    Size,
}

#[derive(Debug, Clone, Default)]
struct FrontLayerScores {
    nodes: Vec<[usize; 2]>,
    // Keep every front-layer partner: a logical qubit may occur in several
    // simultaneously ready two-qubit operations.
    qubits: Vec<Vec<usize>>,
}

impl FrontLayerScores {
    /// Builds a front-layer index retaining all incident partner qubits.
    fn from_ctx(ctx: &SwapSelectionContext<'_>) -> Self {
        let mut out = Self {
            nodes: Vec::new(),
            qubits: vec![Vec::new(); ctx.topology().num_qubits()],
        };
        for pair in ctx.front_layer().physical_pairs() {
            let [a, b] = *pair;
            out.nodes.push([a, b]);
            out.qubits[a].push(b);
            out.qubits[b].push(a);
        }
        out
    }

    fn len(&self) -> usize {
        self.nodes.len()
    }

    fn is_empty(&self) -> bool {
        self.nodes.is_empty()
    }

    fn is_active(&self, qubit: usize) -> bool {
        !self.qubits[qubit].is_empty()
    }

    fn iter_active(&self) -> impl Iterator<Item = &usize> {
        self.nodes.iter().flatten()
    }

    fn total_score(&self, topology: TopologyView<'_>) -> f64 {
        self.nodes
            .iter()
            .map(|pair| topology.distance(pair[0], pair[1]) as f64)
            .sum()
    }

    /// Computes the exact front-layer distance change, including both
    /// endpoints moving when a candidate swaps two active qubits.
    fn score_delta(&self, swap: (usize, usize), topology: TopologyView<'_>) -> f64 {
        let (a, b) = swap;
        let mut delta = 0.0;
        for &[x, y] in &self.nodes {
            let nx = if x == a {
                b
            } else if x == b {
                a
            } else {
                x
            };
            let ny = if y == a {
                b
            } else if y == b {
                a
            } else {
                y
            };
            delta += topology.distance(nx, ny) as f64
                - topology.distance(x, y) as f64;
        }
        delta
    }

    /// Returns an extra reward for swaps that make one or more front gates
    /// executable immediately, adding a nonlinear preference to the ordinary
    /// shortest-distance heuristic.
    fn completion_bonus(&self, swap: (usize, usize), topology: TopologyView<'_>) -> f64 {
        let (a, b) = swap;
        let mut bonus = 0.0;
        for &[x, y] in &self.nodes {
            let old_distance = topology.distance(x, y);
            let nx = if x == a { b } else if x == b { a } else { x };
            let ny = if y == a { b } else if y == b { a } else { y };
            if old_distance > 1 && topology.distance(nx, ny) == 1 {
                bonus += 1.0;
            }
        }
        bonus
    }
}

#[derive(Debug, Clone)]
struct ExtendedSetScores {
    qubits: Vec<Vec<usize>>,
    pairs: Vec<(usize, usize)>,
    len: usize,
}

impl ExtendedSetScores {
    fn new(num_qubits: usize) -> Self {
        Self {
            qubits: vec![Vec::new(); num_qubits],
            pairs: Vec::new(),
            len: 0,
        }
    }

    fn push(&mut self, a: usize, b: usize) {
        self.qubits[a].push(b);
        self.qubits[b].push(a);
        self.pairs.push((a, b));
        self.len += 1;
    }

    /// Scores lookahead gates with geometric decay, preserving the scale of
    /// the ordinary mean while giving nearer successor gates more influence.
    fn weighted_score_delta(
        &self,
        swap: (usize, usize),
        topology: TopologyView<'_>,
    ) -> f64 {
        if self.pairs.is_empty() {
            return 0.0;
        }

        let (a, b) = swap;
        // Use a slower geometric decay so the router still accounts for
        // several upcoming gates while prioritizing the earliest successors.
        // Discount distant successor obligations more aggressively so that
        // lookahead cannot override an urgently executable front-layer gate.
        // Use a short, strongly prioritized horizon: the next few obligations
        // are usually more predictive than distant DAG successors.
        // Use moderate geometric decay so several near-term obligations
        // contribute without allowing distant gates to dominate.
        // A shorter horizon keeps the next dependency layers influential
        // without letting distant, weakly correlated gates dominate.
        let gamma = 0.60_f64;
        let mut weighted = 0.0;
        let mut weights = 0.0;

        for (index, &(x, y)) in self.pairs.iter().enumerate() {
            let weight = gamma.powi(index.min(32) as i32);
            let nx = if x == a {
                b
            } else if x == b {
                a
            } else {
                x
            };
            let ny = if y == a {
                b
            } else if y == b {
                a
            } else {
                y
            };

            weighted +=
                weight * ((topology.distance(nx, ny) as f64)
                    - (topology.distance(x, y) as f64));
            weights += weight;
        }

        if weights == 0.0 {
            0.0
        } else {
            weighted * (self.pairs.len() as f64) / weights
        }
    }

    fn len(&self) -> usize {
        self.len
    }

    fn is_empty(&self) -> bool {
        self.len == 0
    }

    fn total_score(&self, topology: TopologyView<'_>) -> f64 {
        self.qubits
            .iter()
            .enumerate()
            .flat_map(|(a, others)| {
                others
                    .iter()
                    .map(move |b| topology.distance(a, *b) as f64)
            })
            .sum::<f64>()
            * 0.5
    }

    fn score_delta(&self, swap: (usize, usize), topology: TopologyView<'_>) -> f64 {
        let (a, b) = swap;
        let mut total = 0.0;
        for other in &self.qubits[a] {
            if *other == b {
                continue;
            }
            total += (topology.distance(b, *other) as f64) - (topology.distance(a, *other) as f64);
        }
        for other in &self.qubits[b] {
            if *other == a {
                continue;
            }
            total += (topology.distance(a, *other) as f64) - (topology.distance(b, *other) as f64);
        }
        total
    }
}

fn build_extended_set(ctx: &SwapSelectionContext<'_>, max_size: usize) -> ExtendedSetScores {
    let mut out = ExtendedSetScores::new(ctx.topology().num_qubits());
    if max_size == 0 {
        return out;
    }

    let precomputed = ctx.precomputed_extended_set_logical_pairs();
    if !precomputed.is_empty() {
        for pair in precomputed.iter().take(max_size) {
            out.push(
                ctx.layout().physical_of_logical(pair[0]),
                ctx.layout().physical_of_logical(pair[1]),
            );
        }
        return out;
    }

    let mut required_predecessors = ctx.remaining().remaining_predecessor_counts().to_vec();
    let mut to_visit = ctx.front_layer().node_ids().to_vec();
    let mut decremented = Vec::<(usize, usize)>::new();
    let mut i = 0usize;
    while i < to_visit.len() && out.len() < max_size {
        let node_id = to_visit[i];
        for &successor in ctx.circuit().node(node_id).successors() {
            if let Some((_, amount)) = decremented.iter_mut().find(|(idx, _)| *idx == successor) {
                *amount += 1;
            } else {
                decremented.push((successor, 1));
            }
            required_predecessors[successor] -= 1;
            if required_predecessors[successor] == 0 {
                if let Some((a, b)) = ctx.circuit().node(successor).two_qubit_pair() {
                    out.push(ctx.layout().physical_of_logical(a), ctx.layout().physical_of_logical(b));
                }
                to_visit.push(successor);
            }
        }
        i += 1;
    }

    out
}

/// Enumerate each legal edge incident to the front layer exactly once.
/// Deduplicating active endpoints removes tie-breaking bias when a qubit
/// participates in multiple ready gates, while retaining all legal swaps.
fn enumerate_candidate_swaps(
    topology: TopologyView<'_>,
    front_layer: &FrontLayerScores,
) -> Vec<(usize, usize)> {
    let mut out = Vec::<(usize, usize)>::new();
    let mut seen = vec![false; topology.num_qubits()];

    for &phys in front_layer.iter_active() {
        if seen[phys] {
            continue;
        }
        seen[phys] = true;

        for &neighbor in topology.neighbors(phys) {
            if neighbor > phys || !front_layer.is_active(neighbor) {
                out.push((phys, neighbor));
            }
        }
    }

    out
}

fn choose_dense_layout_subset(
    topology: TopologyView<'_>,
    logical_component_size: usize,
    target_component: &[usize],
) -> Result<Vec<usize>, RouterError> {
    if logical_component_size > target_component.len() {
        return Err(RouterError::Routing(format!(
            "logical component size {logical_component_size} exceeds target component size {}",
            target_component.len()
        )));
    }
    if logical_component_size == target_component.len() {
        return Ok(target_component.to_vec());
    }

    let local_index = target_component
        .iter()
        .enumerate()
        .map(|(local, global)| (*global, local))
        .collect::<HashMap<usize, usize>>();
    let mut local_adj = Array2::<f64>::zeros((target_component.len(), target_component.len()));
    for &global_a in target_component {
        let a = local_index[&global_a];
        for &global_b in topology.neighbors(global_a) {
            if let Some(&b) = local_index.get(&global_b) {
                local_adj[[a, b]] = 1.0;
            }
        }
    }
    let error_matrix = Array2::<f64>::zeros((target_component.len(), target_component.len()));
    let [_, _, best_map] = dense_layout::best_subset(
        logical_component_size,
        local_adj.view(),
        0,
        0,
        false,
        true,
        error_matrix.view(),
    );
    let chosen = best_map
        .into_iter()
        .take(logical_component_size)
        .map(|local| target_component[local])
        .collect::<Vec<_>>();
    ensure_connected_subset(topology, &chosen)?;
    Ok(chosen)
}

fn ensure_connected_subset(topology: TopologyView<'_>, subset: &[usize]) -> Result<(), RouterError> {
    if subset.is_empty() {
        return Ok(());
    }
    let set = subset.iter().copied().collect::<HashSet<_>>();
    let mut seen = HashSet::<usize>::new();
    let mut queue = VecDeque::<usize>::new();
    queue.push_back(subset[0]);
    seen.insert(subset[0]);

    while let Some(node) = queue.pop_front() {
        for &next in topology.neighbors(node) {
            if set.contains(&next) && seen.insert(next) {
                queue.push_back(next);
            }
        }
    }

    if seen.len() != set.len() {
        return Err(RouterError::Routing(
            "selected layout subset is not connected".to_string(),
        ));
    }
    Ok(())
}

fn assign_components_to_target(
    logical_components: &[Vec<usize>],
    target_components: &[Vec<usize>],
) -> Result<Vec<(Vec<usize>, usize)>, RouterError> {
    if logical_components.is_empty() {
        return Ok(Vec::new());
    }

    let mut logical_sorted = logical_components.to_vec();
    logical_sorted.sort_by_key(|component| std::cmp::Reverse(component.len()));

    let mut target_sorted = target_components
        .iter()
        .enumerate()
        .map(|(idx, component)| (idx, component.len()))
        .collect::<Vec<_>>();
    target_sorted.sort_by_key(|(_, size)| std::cmp::Reverse(*size));

    let mut free_capacity = target_sorted
        .iter()
        .map(|(idx, size)| (*idx, *size))
        .collect::<HashMap<usize, usize>>();
    let mut assignments = Vec::<(Vec<usize>, usize)>::new();

    for logical in logical_sorted {
        let size = logical.len();
        let mut chosen = None;
        for (target_idx, _) in &target_sorted {
            let cap = free_capacity.get(target_idx).copied().unwrap_or(0);
            if cap >= size {
                chosen = Some(*target_idx);
                break;
            }
        }
        let Some(target_idx) = chosen else {
            return Err(RouterError::Routing(format!(
                "logical component of size {size} cannot fit any target component"
            )));
        };
        *free_capacity
            .get_mut(&target_idx)
            .expect("selected target component must exist") -= size;
        assignments.push((logical, target_idx));
    }
    Ok(assignments)
}

fn choose_disjoint_aware_layout(ctx: &InitialLayoutContext<'_>) -> Result<Vec<usize>, RouterError> {
    let circuit = ctx.circuit();
    let topology = ctx.topology();
    let num_logical = circuit.num_logical_qubits();
    let used = circuit.used_logical_qubits();
    if used.is_empty() {
        return Ok((0..num_logical).collect());
    }

    let logical_components = circuit.logical_interaction_components();
    let target_components = topology.connected_components();
    if target_components.is_empty() {
        return Err(RouterError::Routing(
            "topology has no connected components".to_string(),
        ));
    }

    // Rank logical qubits by total interaction frequency, strongly favoring
    // the first dependency layer. This places urgent operands in the most
    // central sites while retaining global interaction information.
    let first_layer: HashSet<usize> = circuit
        .first_layer_node_ids()
        .iter()
        .copied()
        .collect();
    let mut logical_degree = vec![0usize; num_logical];
    for node_id in circuit.node_ids() {
        if let Some((a, b)) = circuit.node(node_id).two_qubit_pair() {
            logical_degree[a] += 1;
            logical_degree[b] += 1;
        }
    }
    for &node_id in circuit.first_layer_node_ids() {
        if let Some((a, b)) = circuit.node(node_id).two_qubit_pair() {
            logical_degree[a] += 5;
            logical_degree[b] += 5;
        }
    }

    let assignments = assign_components_to_target(logical_components, target_components)?;
    let mut mapping = vec![usize::MAX; num_logical];
    let mut used_physical = vec![false; topology.num_qubits()];

    for (mut logical_component, target_component_idx) in assignments {
        if logical_component.is_empty() {
            continue;
        }
        let target_component = &target_components[target_component_idx];
        // Greedily anchor highly active logical qubits, then place each
        // remaining qubit close to already placed interaction partners.
        let mut available =
            choose_dense_layout_subset(topology, logical_component.len(), target_component)?;

        logical_component.sort_by(|a, b| {
            logical_degree[*b]
                .cmp(&logical_degree[*a])
                .then_with(|| a.cmp(b))
        });

        for (position, logical) in logical_component.iter().enumerate() {
            let mut best_index = 0usize;
            let mut best_cost = u64::MAX;
            let mut best_degree = 0usize;
            let mut best_has_anchor = false;

            for (index, &physical) in available.iter().enumerate() {
                let mut cost = 0u64;
                let mut anchored = false;

                for node_id in circuit.node_ids() {
                    if let Some((a, b)) = circuit.node(node_id).two_qubit_pair() {
                        let other = if a == *logical {
                            Some(b)
                        } else if b == *logical {
                            Some(a)
                        } else {
                            None
                        };

                        if let Some(other) = other {
                            if mapping[other] != usize::MAX {
                                // Give currently executable obligations extra
                                // weight during placement. This preserves the
                                // global interaction-frequency layout while
                                // explicitly embedding the initial front layer.
                                let urgency = if first_layer.contains(&node_id) {
                                    4u64
                                } else {
                                    1u64
                                };
                                cost += urgency
                                    * topology.distance(physical, mapping[other]) as u64;
                                anchored = true;
                            }
                        }
                    }
                }

                let degree = topology.degree(physical);
                let better = if position == 0 {
                    degree > best_degree
                        || (degree == best_degree && physical < available[best_index])
                } else {
                    (anchored && !best_has_anchor)
                        || (anchored == best_has_anchor
                            && (cost < best_cost
                                || (cost == best_cost && degree > best_degree)))
                };

                if better {
                    best_index = index;
                    best_cost = cost;
                    best_degree = degree;
                    best_has_anchor = anchored;
                }
            }

            let physical = available.swap_remove(best_index);
            mapping[*logical] = physical;
            used_physical[physical] = true;
        }
    }

    let mut free_physical = (0..topology.num_qubits()).filter(|q| !used_physical[*q]);
    for slot in &mut mapping {
        if *slot == usize::MAX {
            *slot = free_physical.next().ok_or_else(|| {
                RouterError::Routing("not enough physical qubits to complete layout".to_string())
            })?;
        }
    }
    Ok(mapping)
}

#[derive(Debug, Clone)]
pub struct CandidatePolicy {
    pub basic_weight: f64,
    pub lookahead_weight: f64,
    pub lookahead_size: usize,
    pub set_scaling: SetScaling,
    pub use_decay: bool,
    pub decay_increment: f64,
    pub decay_reset: usize,
    pub best_epsilon: f64,
    decay_state: Vec<f64>,
}

impl Default for CandidatePolicy {
    fn default() -> Self {
        Self {
            basic_weight: 1.0,
            lookahead_weight: 0.5,
            // A shorter horizon keeps immediate front-layer progress dominant
            // on sparse devices while still anticipating successor gates.
            lookahead_size: 16,
            set_scaling: SetScaling::Size,
            use_decay: true,
            // Keep the standard SABRE decay gentle: strong endpoint penalties
            // can force unnecessary detours on narrow or highly branched graphs.
            decay_increment: 0.001,
            decay_reset: 5,
            best_epsilon: 1e-10,
            decay_state: Vec::new(),
        }
    }
}

impl CandidatePolicy {
    /// Updates endpoint tabu penalties, resetting them after progress or a
    /// short stagnation window.
    fn refresh_decay_state(&mut self, ctx: &SwapSelectionContext<'_>) {
        if !self.use_decay {
            return;
        }

        let num_qubits = ctx.topology().num_qubits();
        if self.decay_state.len() != num_qubits {
            self.decay_state = vec![1.0; num_qubits];
        }

        if ctx.swaps_since_progress() == 0 {
            self.decay_state.fill(1.0);
            return;
        }

        let reset = self.decay_reset.max(1);
        if ctx.swaps_since_progress() % reset == 0 {
            self.decay_state.fill(1.0);
        } else if let Some((a, b)) = ctx.last_applied_swap() {
            // Narrow, low-branching graphs often require repeatedly using the
            // same corridor endpoints; soften decay there to avoid forcing
            // unnecessary detours, while preserving stronger diversification
            // on well-connected topologies.
            let average_degree = if num_qubits == 0 {
                0.0
            } else {
                2.0 * ctx.topology().num_edges() as f64 / num_qubits as f64
            };
            let topology_factor = if average_degree < 2.5 { 0.5 } else { 1.0 };
            let increment = self.decay_increment * topology_factor;
            self.decay_state[a] += increment;
            self.decay_state[b] += increment;
        }
    }
}

impl Policy for CandidatePolicy {
    fn choose_best_initial_layout(
        &mut self,
        ctx: &InitialLayoutContext<'_>,
        _rng: &mut RngState,
    ) -> Result<Vec<usize>, RouterError> {
        self.decay_state.clear();
        choose_disjoint_aware_layout(ctx)
    }

    fn choose_best_swap(
        &mut self,
        ctx: &SwapSelectionContext<'_>,
        rng: &mut RngState,
    ) -> Option<(usize, usize)> {
        self.refresh_decay_state(ctx);

        let front_layer = FrontLayerScores::from_ctx(ctx);
        let candidates = enumerate_candidate_swaps(ctx.topology(), &front_layer);
        if candidates.is_empty() {
            return None;
        }

        let extended_set = build_extended_set(ctx, self.lookahead_size);

        let scale = |weight: f64, size: usize, scaling: SetScaling| -> f64 {
            match scaling {
                SetScaling::Constant => weight,
                SetScaling::Size => {
                    if size == 0 {
                        0.0
                    } else {
                        weight / (size as f64)
                    }
                }
            }
        };

        let basic_weight = scale(self.basic_weight, front_layer.len(), self.set_scaling);
        // Normalize the extended-set contribution by its effective
        // geometric horizon rather than allowing a long horizon to dominate
        // the front layer.
        // Keep lookahead influential during normal routing, but progressively
        // prioritize the front layer when swaps have not produced progress.
        // This acts as a lightweight release valve without changing legality.
        let lookahead_weight = if extended_set.is_empty() {
            0.0
        } else {
            // Use less speculative lookahead on narrow, low-branching
            // topologies, where detours tend to be costly.  On broader
            // graphs, path diversity makes upcoming gates more predictive.
            let topology = ctx.topology();
            let average_degree = if topology.num_qubits() == 0 {
                0.0
            } else {
                2.0 * topology.num_edges() as f64 / topology.num_qubits() as f64
            };
            let topology_factor = if average_degree < 2.5 {
                0.55
            } else if average_degree < 3.0 {
                0.80
            } else {
                1.0
            };

            // After several non-progressing swaps, prioritize the front
            // layer and act as a release valve against heuristic cycles.
            let stagnation_factor = if ctx.swaps_since_progress() >= 3 {
                0.70
            } else if ctx.swaps_since_progress() >= 1 {
                0.85
            } else {
                1.0
            };

            scale(
                self.lookahead_weight,
                extended_set.len(),
                self.set_scaling,
            ) * stagnation_factor
                * topology_factor
        };

        let mut swap_scores = candidates
            .iter()
            .copied()
            .map(|swap| (swap, 0.0))
            .collect::<Vec<_>>();

        // Score candidate-induced distance changes. In addition to the
        // linear distance reduction, reward completing a front gate. During
        // stagnation this reward is increased, steering the router toward
        // definite progress instead of speculative lookahead improvements.
        // Completing a front-layer gate is substantially more valuable than
        // merely reducing its distance: it immediately releases successors
        // and can change the candidate set on the next routing step.
        // Definite front-layer completion is more valuable than a small
        // distance improvement because it releases dependent DAG nodes.
        // Increase this preference during stagnation to guarantee decisive
        // progress whenever a candidate can bring a gate onto an edge.
        let completion_weight = if ctx.swaps_since_progress() >= 2 {
            0.65
        } else {
            0.40
        };
        for (swap, score) in &mut swap_scores {
            *score += basic_weight * front_layer.score_delta(*swap, ctx.topology());
            *score -= basic_weight
                * completion_weight
                * front_layer.completion_bonus(*swap, ctx.topology());
        }

        if !extended_set.is_empty() && self.lookahead_weight != 0.0 {
            for (swap, score) in &mut swap_scores {
                // A geometrically decayed horizon avoids allowing distant,
                // weakly correlated gates to override the next few gates.
                *score +=
                    lookahead_weight * extended_set.weighted_score_delta(*swap, ctx.topology());


            }
        }

        // Apply standard SABRE multiplicative decay to relative score changes.
        // This preserves the proven anti-cycling behavior and avoids adding a
        // topology-scaled absolute penalty that can distort close candidates.
        if self.use_decay {
            for (swap, score) in &mut swap_scores {
                *score *= self.decay_state[swap.0].max(self.decay_state[swap.1]);
            }
        }

        let mut min_score = f64::INFINITY;
        let mut best_swaps = Vec::<(usize, usize)>::new();
        for (swap, score) in &swap_scores {
            if *score + self.best_epsilon < min_score {
                min_score = *score;
                best_swaps.clear();
                best_swaps.push(*swap);
                continue;
            }
            if (*score - min_score).abs() <= self.best_epsilon {
                best_swaps.push(*swap);
            }
        }

        Some(best_swaps[rng.gen_index(best_swaps.len())])
    }
}
// EVOLVE-BLOCK-END

Requires the original benchmark harness and dependencies.

Score history 100 iterations

Score history

Best-so-far search-time score ↑ · each new best is colored by that iteration’s gate decision

RetrieveLook-UpNo-Op
Swap Reduction · recorded search-time scores6,38710,311.414,235.818,160.20255075100Outer-loop iterationIteration 1 · Retrieve · new best 6,498Iteration 2 · Retrieve · new best 6,565.8Iteration 3 · Retrieve · new best 6,957Iteration 5 · Retrieve · new best 11,325.6Iteration 7 · Retrieve · new best 15,174Iteration 8 · Retrieve · new best 15,341.4Iteration 9 · Retrieve · new best 15,493.8Iteration 13 · Retrieve · new best 16,871.4Iteration 15 · Retrieve · new best 17,197.8Iteration 28 · Retrieve · new best 17,201.4Iteration 66 · Retrieve · new best 17,608.2Iteration 71 · Retrieve · new best 17,672.4Iteration 75 · Retrieve · new best 17,784.6Iteration 77 · Retrieve · new best 17,875.8Iteration 78 · Retrieve · new best 18,160.2
Gate decisionsIterations 1–100 · Retrieve 94 · Look-Up 4 · No-Op 2

Search-time scores; the final native objective is reported above.

Run details

A quantum circuit router that combines depth-weighted lookahead, topology-aware scoring, and penalties for reversing recent SWAPs.

The improvement is specific to Q20 under the shared evaluator. It is not a claim of superiority on every device topology.

Recorded score: 14835 · reference: 15186 · seed: 42.

Program ID: 90c289d7-c45d-4884-b42a-0cd51152bbca
Source SHA-256: 1d25829c5f485742c5ec62046d6f4e3903280ed2207980bd8b5c610d7fdf015b
History SHA-256: 8d512aaa255fc014b8f47fcec657f850d1a380976a3eda01bc2fde2a76f604f9

Scientific visualization