Skip to content

Interactive ASCAT-like Purity/Ploidy Fitting

Because a bulk tumor sample contains both tumor and normal cells, its observed LogR and B-allele frequency (BAF) depend on tumor purity and ploidy. ASCAT estimates these parameters using a grid search over candidate combinations, favoring those that produce allele-specific copy-number estimates close to nonnegative integers, as described in Allele-specific copy number analysis of tumors.

This interactive GenomeSpy example lets you explore how different purity/ploidy combinations affect the inferred allele-specific copy numbers and their fit to the observed data.

The top "sunrise" plot shows integer-fit distance over a grid of tumor purity (rho) and tumor ploidy (psi). Color shows the mean squared distance on a logarithmic scale. Blue cells have lower distances, meaning that the included allele-specific estimates are closer to integer copy numbers.

Click or drag anywhere on the sunrise plot to choose a fit. The readout below the heatmap evaluates the exact chosen coordinates, and the other panels update the allele-specific copy numbers and fitted LogR and BAF overlays.

The sample dropdown below the visualization switches between several simulated tumors. Its selection reloads the segment and raw-probe data and recomputes the sunrise plot and fitted tracks.

{
  "description": [
    "Interactive ASCAT-like Purity/Ploidy Fitting",
    "Visualizes ASCAT purity/ploidy fits for simulated samples and shows how rounded allele-specific copy-number estimates change when rho and psi are adjusted.",
    "Loo P Van, Nordgard SH, Lingjærde OC, et al.",
    "Allele-specific copy number analysis of tumors",
    "Proc Natl Acad Sci. 2010;107(39):16910-16915. doi:10.1073/pnas.1009843107"
  ],

  "assembly": "hg19",

  "params": [
    { "name": "minLength", "value": 1 },
    { "name": "selectedFit" },
    {
      "name": "sample",
      "value": "S96",
      "bind": {
        "input": "select",
        "options": [
          "S17",
          "S36",
          "S54",
          "S64",
          "S77",
          "S84",
          "S96",
          "S97",
          "S100"
        ],
        "name": "Sample"
      }
    },
    {
      "name": "gamma",
      "value": 0.55,
      "bind": {
        "input": "range",
        "min": 0.2,
        "max": 1.2,
        "step": 0.05,
        "name": "LogR decompaction (gamma)",
        "debounce": 100,
        "description": "Controls LogR decompaction in the ASCAT copy-number equations."
      }
    },
    {
      "name": "downweightBalanced",
      "value": true,
      "bind": {
        "input": "checkbox",
        "name": "Downweight balanced segments",
        "description": "Multiply the heterozygous-probe-count weight of exactly balanced segments by 0.05, as in ASCAT R v3.2.0."
      }
    },
    {
      "name": "fitBothAlleles",
      "value": false,
      "bind": {
        "input": "checkbox",
        "name": "Include both alleles in fit",
        "description": "Include major-allele rounding error alongside minor-allele rounding error in the sunrise and selected-fit score."
      }
    }
  ],

  "data": {
    "url": {
      "expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_fit_segments_' + sample + '.tsv.gz'"
    }
  },

  "transform": [
    {
      "type": "collect"
    },
    {
      "type": "formula",
      "expr": "(selectedFit.values.y - 1 + pow(2, datum.logRMean / gamma) * (1 - datum.bafMean) * (2 * (1 - selectedFit.values.y) + selectedFit.values.y * selectedFit.values.x)) / selectedFit.values.y",
      "as": "aRaw",
      "description": "Apply ASCAT Supporting Information Eq. S7 to estimate raw major-allele copy number at the selected purity and tumor ploidy."
    },
    {
      "type": "formula",
      "expr": "(selectedFit.values.y - 1 + pow(2, datum.logRMean / gamma) * datum.bafMean * (2 * (1 - selectedFit.values.y) + selectedFit.values.y * selectedFit.values.x)) / selectedFit.values.y",
      "as": "bRaw",
      "description": "Apply ASCAT Supporting Information Eq. S8 to estimate raw minor-allele copy number at the selected purity and tumor ploidy."
    },
    {
      "type": "formula",
      "expr": "max(0, round(datum.aRaw))",
      "as": "nMajor",
      "description": "Round the raw major-allele estimate to a nonnegative integer copy number as in Supporting Information Eq. S10."
    },
    {
      "type": "formula",
      "expr": "max(0, round(datum.bRaw))",
      "as": "nMinor",
      "description": "Round the raw minor-allele estimate to a nonnegative integer copy number as in Supporting Information Eq. S11."
    },
    {
      "type": "formula",
      "expr": "abs(datum.aRaw - datum.nMajor)",
      "as": "aError"
    },
    {
      "type": "formula",
      "expr": "abs(datum.bRaw - datum.nMinor)",
      "as": "bError"
    },
    {
      "type": "formula",
      "expr": "gamma * log((2 * (1 - selectedFit.values.y) + selectedFit.values.y * (datum.nMajor + datum.nMinor)) / (2 * (1 - selectedFit.values.y) + selectedFit.values.y * selectedFit.values.x)) / LN2",
      "as": "logRMean_ASCAT",
      "description": "Project the rounded copy numbers back to fitted LogR using Supporting Information Eq. S12."
    },
    {
      "type": "formula",
      "expr": "(1 - selectedFit.values.y + selectedFit.values.y * datum.nMinor) / (2 - 2 * selectedFit.values.y + selectedFit.values.y * (datum.nMajor + datum.nMinor))",
      "as": "bafMean_ASCAT",
      "description": "Project the rounded copy numbers back to fitted BAF using Supporting Information Eq. S13."
    },
    {
      "type": "identifier"
    }
  ],

  "resolve": { "axis": { "x": "shared" } },

  "encoding": {
    "x": {
      "chrom": "chr",
      "pos": "startpos",
      "type": "locus",
      "scale": { "type": "locus" },
      "axis": { "title": null }
    },
    "x2": {
      "chrom": "chr",
      "pos": "endpos",
      "offset": 1
    }
  },

  "vconcat": [
    {
      "name": "sunrise",

      "title": {
        "text": {
          "expr": "'Purity/ploidy integer-fit distance - ' + sample"
        },
        "style": "overlay-title"
      },

      "height": 240,

      "params": [
        {
          "name": "selectedFit",
          "push": "outer",
          "value": { "x": 2.75, "y": 0.56 },
          "ruler": {
            "encodings": ["x", "y"],
            "on": "mousedown",
            "clear": false,
            "snap": false,
            "mark": {
              "strokeDash": [5, 3]
            }
          }
        }
      ],

      "data": {
        "sequence": {
          "start": 0.1,
          "stop": 1.051,
          "step": 0.01,
          "as": "rhoCandidate"
        }
      },

      "transform": [
        {
          "type": "cross",
          "description": "Form the purity and tumor-ploidy grid used by the ASCAT parameter search.",
          "from": {
            "data": {
              "sequence": {
                "start": 1,
                "stop": 6.001,
                "step": 0.05,
                "as": "psiCandidate"
              }
            }
          }
        },
        {
          "type": "cross",
          "description": "Cross every purity/ploidy candidate with ASCAT's joint segmented LogR/BAF runs for distance evaluation.",
          "from": {
            "data": {
              "url": {
                "expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_fit_segments_' + sample + '.tsv.gz'"
              }
            },
            "transform": [
              {
                "type": "filter",
                "expr": "datum.chr !== 'X'"
              },
              {
                "type": "project",
                "fields": ["logRMean", "bafMean", "nProbes"]
              },
              {
                "type": "formula",
                "expr": "datum.nProbes * (downweightBalanced && datum.bafMean === 0.5 ? 0.05 : 1)",
                "as": "fitWeight",
                "description": "Weight each joint run by its heterozygous-probe count and, optionally, downweight exactly balanced runs 20-fold as in ASCAT R v3.2.0."
              }
            ]
          }
        },
        {
          "type": "formula",
          "expr": "(datum.rhoCandidate - 1 + pow(2, datum.logRMean / gamma) * datum.bafMean * (2 * (1 - datum.rhoCandidate) + datum.rhoCandidate * datum.psiCandidate)) / datum.rhoCandidate",
          "as": "bRawCandidate",
          "description": "Apply ASCAT Supporting Information Eq. S8 to estimate minor-allele copy number for each candidate and segment."
        },
        {
          "type": "formula",
          "expr": "(datum.rhoCandidate - 1 + pow(2, datum.logRMean / gamma) * (1 - datum.bafMean) * (2 * (1 - datum.rhoCandidate) + datum.rhoCandidate * datum.psiCandidate)) / datum.rhoCandidate",
          "as": "aRawCandidate",
          "description": "Apply ASCAT Supporting Information Eq. S7 to estimate major-allele copy number when both alleles are included in the fit."
        },
        {
          "type": "formula",
          "expr": "max(0, round(datum.bRawCandidate))",
          "as": "nMinorCandidate",
          "description": "Round each candidate's raw minor-allele estimate to a nonnegative integer copy number."
        },
        {
          "type": "formula",
          "expr": "max(0, round(datum.aRawCandidate))",
          "as": "nMajorCandidate",
          "description": "Round each candidate's raw major-allele estimate to a nonnegative integer copy number."
        },
        {
          "type": "formula",
          "expr": "(pow(datum.bRawCandidate - datum.nMinorCandidate, 2) + (fitBothAlleles ? pow(datum.aRawCandidate - datum.nMajorCandidate, 2) : 0)) * datum.fitWeight",
          "as": "errorSquaredWeighted",
          "description": "Evaluate uncapped squared distance to nonnegative integers using the minor allele alone or both alleles, weighted by ASCAT's probe-based run weight."
        },
        {
          "type": "aggregate",
          "groupby": ["rhoCandidate", "psiCandidate"],
          "fields": ["fitWeight", "errorSquaredWeighted"],
          "ops": ["sum", "sum"],
          "as": ["totalWeight", "distanceSum"],
          "description": "Aggregate the selected allele-specific integer-fit distance for each purity and tumor-ploidy candidate."
        },
        {
          "type": "formula",
          "expr": "datum.distanceSum / (datum.totalWeight * (fitBothAlleles ? 2 : 1))",
          "as": "meanRoundingError",
          "description": "Normalize the summed error over the included alleles into the mean integer-fit distance displayed by the sunrise plot."
        },
        {
          "type": "formula",
          "expr": "max(0.1, datum.rhoCandidate - 0.005)",
          "as": "rhoStart"
        },
        {
          "type": "formula",
          "expr": "min(1.05, datum.rhoCandidate + 0.005)",
          "as": "rhoEnd"
        },
        {
          "type": "formula",
          "expr": "max(1, datum.psiCandidate - 0.025)",
          "as": "psiStart"
        },
        {
          "type": "formula",
          "expr": "min(6, datum.psiCandidate + 0.025)",
          "as": "psiEnd"
        }
      ],

      "resolve": {
        "scale": { "x": "excluded" },
        "axis": { "x": "excluded" }
      },

      "mark": {
        "type": "rect",
        "tooltip": null
      },

      "encoding": {
        "x": {
          "field": "psiStart",
          "type": "quantitative",
          "scale": { "domain": [1, 6] },
          "axis": { "title": "Tumor ploidy (psi)", "format": ".1f" }
        },
        "x2": { "field": "psiEnd" },
        "y": {
          "field": "rhoStart",
          "type": "quantitative",
          "scale": { "domain": [0.1, 1.05] },
          "axis": { "title": "Tumor purity (rho)", "format": ".2f" }
        },
        "y2": { "field": "rhoEnd" },
        "color": {
          "field": "meanRoundingError",
          "type": "quantitative",
          "scale": {
            "scheme": "redblue",
            "type": "log",
            "reverse": true
          }
        }
      }
    },

    {
      "resolve": { "axis": { "x": "shared" } },

      "vconcat": [
        {
          "name": "selectedGoodnessOfFit",

          "title": {
            "text": "Selected fit",
            "orient": "none"
          },

          "height": 24,

          "resolve": {
            "scale": { "x": "excluded" },
            "axis": { "x": "excluded" }
          },

          "transform": [
            {
              "type": "filter",
              "expr": "datum.chr !== 'X'"
            },
            {
              "type": "project",
              "fields": ["bafMean", "aError", "bError", "nProbes"]
            },
            {
              "type": "formula",
              "expr": "datum.nProbes * (downweightBalanced && datum.bafMean === 0.5 ? 0.05 : 1)",
              "as": "fitWeight",
              "description": "Apply the same ASCAT probe-based and optional balanced-run weighting as the sunrise."
            },
            {
              "type": "formula",
              "expr": "(datum.bError * datum.bError + (fitBothAlleles ? datum.aError * datum.aError : 0)) * datum.fitWeight",
              "as": "errorSquaredWeighted",
              "description": "Evaluate the selected fit with the same weighted allele-specific error as the sunrise."
            },
            {
              "type": "aggregate",
              "fields": ["fitWeight", "errorSquaredWeighted"],
              "ops": ["sum", "sum"],
              "as": ["totalWeight", "distanceSum"],
              "description": "Aggregate the selected fit's allele-specific error over all autosomal joint runs."
            },
            {
              "type": "formula",
              "expr": "100 - datum.distanceSum / (datum.totalWeight * (fitBothAlleles ? 2 : 1) * 0.25) * 100",
              "as": "goodnessOfFit",
              "description": "Convert the selected mean distance to an ASCAT-like goodness score; the publication defines the original rescaling after Eq. 3."
            }
          ],

          "layer": [
            {
              "name": "bar",

              "mark": {
                "type": "rect",
                "color": "#a0e7e5",
                "tooltip": null
              },

              "encoding": {
                "x": {
                  "field": "goodnessOfFit",
                  "type": "quantitative",
                  "scale": {
                    "domain": [0, 100],
                    "clamp": true
                  },
                  "axis": null
                },

                "x2": { "datum": 0 }
              }
            },
            {
              "name": "text",

              "mark": {
                "type": "text",
                "size": 12,
                "tooltip": null
              },

              "encoding": {
                "x": { "value": 0.5 },
                "x2": null,
                "text": {
                  "expr": "'rho ' + (round(selectedFit.values.y * 1000) / 1000) + ' | psi ' + (round(selectedFit.values.x * 1000) / 1000) + ' | gamma ' + gamma + ' | score ' + (round(datum.goodnessOfFit * 100) / 100)"
                }
              }
            }
          ]
        },
        {
          "name": "roundedAndDifference",
          "title": {
            "text": "Copy numbers rounded to integers + difference to raw values",
            "style": "overlay"
          },

          "height": { "grow": 2 },

          "layer": [
            {
              "title": "mismatchMinor",
              "mark": {
                "type": "rect",
                "minWidth": { "expr": "minLength" },
                "fillOpacity": 0.15,
                "strokeOpacity": 1,
                "strokeWidth": 1,
                "fill": "gray",
                "stroke": "#88d27a"
              },
              "encoding": {
                "y": {
                  "field": "bRaw",
                  "type": "quantitative"
                },
                "y2": {
                  "field": "nMinor"
                },
                "strokeOpacity": {
                  "field": "bError",
                  "type": "quantitative",
                  "scale": {
                    "type": "pow",
                    "base": 2,
                    "domain": [0, 0.5],
                    "range": [0, 0.8]
                  }
                }
              }
            },
            {
              "name": "mismatchMajor",
              "title": "mismatchMajor",
              "mark": {
                "type": "rect",
                "minWidth": { "expr": "minLength" },
                "fillOpacity": 0.15,
                "strokeOpacity": 1,
                "strokeWidth": 1,
                "fill": "gray",
                "stroke": "#f06850"
              },
              "encoding": {
                "y": {
                  "field": "aRaw",
                  "type": "quantitative"
                },
                "y2": {
                  "field": "nMajor"
                },
                "strokeOpacity": {
                  "field": "aError",
                  "type": "quantitative",
                  "scale": {
                    "type": "pow",
                    "base": 2,
                    "domain": [0, 0.5],
                    "range": [0, 0.8]
                  }
                }
              }
            },
            {
              "title": "nMinor",
              "mark": {
                "type": "rule",
                "minLength": { "expr": "minLength" },
                "yOffset": -3.0
              },
              "encoding": {
                "y": {
                  "field": "nMinor",
                  "type": "quantitative",
                  "scale": {
                    "domain": [0, 6],
                    "padding": 0.04,
                    "clamp": true
                  },
                  "axis": {
                    "tickMinStep": 1.0
                  }
                },
                "size": { "value": 5 },
                "color": { "value": "#88d27a" }
              }
            },
            {
              "title": "nMajor",
              "mark": {
                "type": "rule",
                "minLength": { "expr": "minLength" },
                "yOffset": 3.0
              },
              "encoding": {
                "y": {
                  "field": "nMajor",
                  "type": "quantitative",
                  "scale": {
                    "domain": [0, 6]
                  }
                },
                "size": { "value": 5 },
                "color": {
                  "field": "nMajor",
                  "type": "quantitative",
                  "scale": {
                    "domain": [0, 6, 16],
                    "range": ["#f06850", "#f06850", "#5F0F0F"]
                  }
                }
              }
            }
          ]
        }
      ]
    },

    {
      "name": "logRTrack",

      "title": {
        "text": "Observed and fitted LogR",
        "style": "overlay-title"
      },
      "layer": [
        {
          "data": {
            "url": {
              "expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_raw_' + sample + '.tsv.gz'"
            }
          },

          "title": "Single probe",

          "mark": {
            "type": "point",
            "size": {
              "expr": "min(pow(zoomLevel, 2) * width * height / 10000, 100)"
            },
            "color": "#aab",
            "opacity": 0.3
          },

          "encoding": {
            "x": {
              "chrom": "chr",
              "pos": "pos",
              "type": "locus"
            },
            "y": { "field": "logR", "type": "quantitative", "title": null }
          }
        },
        {
          "title": "Mean LogR",
          "mark": {
            "type": "rule",
            "minLength": { "expr": "minLength" }
          },
          "encoding": {
            "y": {
              "field": "logRMean",
              "type": "quantitative",
              "title": "LogR"
            },
            "size": { "value": 3 },
            "color": { "value": "black" }
          }
        },
        {
          "title": "ASCAT-like fitted LogR",
          "mark": {
            "type": "rule",
            "minLength": { "expr": "minLength" }
          },
          "encoding": {
            "y": {
              "field": "logRMean_ASCAT",
              "type": "quantitative",
              "title": null
            },
            "size": { "value": 2 },
            "color": { "value": "#f06850" }
          }
        }
      ]
    },

    {
      "name": "bafTrack",

      "title": {
        "text": "Observed and fitted B-allele frequency",
        "style": "overlay-title"
      },

      "layer": [
        {
          "data": {
            "url": {
              "expr": "'https://data.genomespy.app/sample-data/ASCAT/ascat_raw_' + sample + '.tsv.gz'"
            }
          },

          "transform": [{ "type": "filter", "expr": "datum.baf !== null" }],

          "title": "Single probe",

          "mark": {
            "type": "point",
            "size": {
              "expr": "min(pow(zoomLevel, 2) * width * height / 10000, 100)"
            },
            "color": "#aab",
            "opacity": 0.3
          },

          "encoding": {
            "x": {
              "chrom": "chr",
              "pos": "pos",
              "type": "locus"
            },
            "y": { "field": "baf", "type": "quantitative", "title": null }
          }
        },
        {
          "title": "Mean BAF",
          "mark": {
            "type": "rule",
            "minLength": { "expr": "minLength" }
          },
          "encoding": {
            "y": {
              "field": "bafMean",
              "type": "quantitative",
              "scale": { "domain": [0, 1] },
              "title": "BAF"
            },
            "size": { "value": 3 },
            "color": { "value": "black" }
          }
        },
        {
          "title": "Mean BAF",
          "mark": {
            "type": "rule",
            "minLength": 3.0
          },
          "encoding": {
            "y": {
              "expr": "1 - datum.bafMean",
              "type": "quantitative",
              "title": null
            },
            "size": { "value": 3 },
            "color": { "value": "black" }
          }
        },
        {
          "title": "ASCAT-like fitted minor-allele BAF",
          "mark": {
            "type": "rule",
            "minLength": { "expr": "minLength" }
          },
          "encoding": {
            "y": {
              "field": "bafMean_ASCAT",
              "type": "quantitative",
              "title": null
            },
            "size": { "value": 2 },
            "color": { "value": "#88d27a" }
          }
        },
        {
          "title": "ASCAT-like fitted major-allele BAF",
          "mark": {
            "type": "rule",
            "minLength": { "expr": "minLength" }
          },
          "encoding": {
            "y": {
              "expr": "1 - datum.bafMean_ASCAT",
              "type": "quantitative",
              "title": null
            },
            "size": { "value": 2 },
            "color": { "value": "#f06850" }
          }
        }
      ]
    }
  ],

  "background": "#fafafa",

  "config": {
    "axisX": {
      "grid": false,
      "chromGrid": true,
      "orient": "bottom"
    },
    "axisY": {
      "grid": true,
      "gridColor": "#f8f8f8"
    },
    "legend": { "disable": true },
    "view": {
      "fill": "white",
      "stroke": "#c8c8c8",
      "shadowBlur": 8,
      "shadowColor": "black",
      "shadowOpacity": 0.1
    }
  }
}

The visualization uses simulated example data from Allele-specific copy number analysis of tumors by Loo et al. and follows the ASCAT method described there. It is an explanatory ASCAT-like fit implemented as a GenomeSpy specification. The input has already been segmented using ASPCF; this visualization does not perform segmentation. The raw LogR and BAF probe values are shown only as a reference for the segmented and fitted values.

What to notice

Look for multiple local minima in the sunrise plot. Different purity/ploidy combinations can make the inferred copy numbers similarly close to integers, but a low distance is not necessarily the correct biological solution. For example, a solution may retain at least one minor-allele copy in every segment, implying no complete loss of heterozygosity. Whether this is plausible depends on the tumor. Many low-purity profiles also present a low-distance minima at 100% purity.

As the ruler moves, the gray mismatch bands show where the inferred major- or minor-allele copy numbers are farthest from integers. The colored fitted overlays project the integer copy numbers back into LogR and BAF space. A mismatch between these overlays and the black observed segment means shows what the integer-fit distance does not capture.

Fitting model

The original paper denotes the aberrant cell fraction by rho and tumor ploidy by psi_t. It uses psi = 2 * (1 - rho) + rho * psi_t for the average ploidy of the mixed sample. For compactness, the GenomeSpy specification calls the tumor-ploidy parameter psi; thus, its psi corresponds to the paper's psi_t.

The global gamma slider controls how strongly compressed LogR values are expanded before copy numbers are calculated. Changing it recomputes the entire sunrise plot and shows how this choice affects the purity/ploidy grid.

For every segment and candidate pair (rho, psi), the spec converts the observed logRMean and bafMean into raw major- and minor-allele copy-number estimates. These are Eqs. S7 and S8 in the ASCAT Supporting Information, which invert the LogR and BAF mixture model in Eqs. 1 and 2 of the paper:

  • aRaw = (rho - 1 + 2^(logRMean / gamma) * (1 - bafMean) * (2 * (1 - rho) + rho * psi)) / rho
  • bRaw = (rho - 1 + 2^(logRMean / gamma) * bafMean * (2 * (1 - rho) + rho * psi)) / rho

Each estimate is rounded to a nonnegative integer. The input BAF values are mirrored into [0, 0.5], which guarantees that bRaw is the minor-allele estimate. Each segment is weighted by its number of germline-heterozygous probes. When balanced-segment downweighting is enabled, segments with bafMean = 0.5 receive 5% of that weight, matching ASCAT R v3.2.0:

  • weight = nProbes * (bafMean == 0.5 ? 0.05 : 1)
  • error = weight * (bRaw - max(round(bRaw), 0))^2
  • meanRoundingError = sum(error) / sum(weight)
  • score = 100 - 100 * meanRoundingError / 0.25

The balanced-segment checkbox exposes this weighting choice interactively. It is especially important for low-purity samples, where many BAF estimates are too close to 0.5 to distinguish from balanced segments.

Higher scores indicate a better integer fit. The error has no upper limit, so strongly negative raw copy-number estimates can produce scores below zero.

By default, the visualization follows ASCAT R v3.2.0 and scores only the minor-allele estimate (bRaw). The "Include both alleles in fit" toggle adds the major-allele error. With balanced-segment weighting enabled, this matches paper Eq. 3 apart from a constant scaling that does not change the locations of the minima.

The visualization does not select an optimum: rho and psi come directly from the interactive ruler.

The colored overlays in the lower tracks show what the rounded copy-number states would look like in LogR and BAF space. The rounding follows Eqs. S10 and S11, while the fitted LogR and BAF values use Eqs. S12 and S13. gamma is applied in both directions so that the calculations remain consistent.

Sampled grid and exact selection

The sunrise heatmap samples rho from 0.10 to 1.05 in steps of 0.01 and psi from 1.00 to 6.00 in steps of 0.05, matching ASCAT R v3.2.0's default calculated distance matrix. GenomeSpy builds the grid by combining every purity/ploidy pair with every segment, calculating the errors, and summarizing the result for each pair.

The ruler does not snap to the sampled grid. It can select fractional values between cells, and the score and fitted tracks are calculated directly from those exact values rather than from the nearest heatmap cell.

GenomeSpy features

GenomeSpy calculates the analysis in the browser from a declarative specification rather than loading a precomputed grid. The cross, formula, and aggregate transforms combine every candidate pair with the segments, evaluate the ASCAT-like equations, and calculate the probe-weighted distances.

Global parameters control gamma and the fitting options. Changing them recomputes the affected data. A two-dimensional ruler parameter supplies rho and psi to the linked views. It calculates the score and fitted profile at exact values between sampled grid cells. vconcat and layer combine the grid, score, copy-number, LogR, and BAF views, while the locus scale aligns the genomic tracks. No application-specific JavaScript is needed.