<commit_before>package dr.evomodel.antigenic;

import dr.evolution.util.*;
import dr.inference.model.*;
import dr.math.MathUtils;
import dr.math.distributions.NormalDistribution;
import dr.util.*;
import dr.xml.*;

import java.io.*;
import java.util.*;
import java.util.logging.Logger;

/**
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @version $Id$
 */
public class AntigenicLikelihood extends AbstractModelLikelihood implements Citable {
    private static final boolean CHECK_INFINITE = false;
    private static final boolean USE_THRESHOLDS = true;
    private static final boolean USE_INTERVALS = true;

    public final static String ANTIGENIC_LIKELIHOOD = "antigenicLikelihood";

    // column indices in table
    private static final int COLUMN_LABEL = 0;
    private static final int SERUM_STRAIN = 2;
    private static final int ROW_LABEL = 1;
    private static final int VIRUS_STRAIN = 3;
    private static final int SERUM_DATE = 4;
    private static final int VIRUS_DATE = 5;
    private static final int TITRE = 6;

    public enum MeasurementType {
        INTERVAL,
        POINT,
        THRESHOLD,
        MISSING
    }

    public AntigenicLikelihood(
            int mdsDimension,
            Parameter mdsPrecisionParameter,
            TaxonList strainTaxa,
            MatrixParameter locationsParameter,
            CompoundParameter tipTraitsParameter,
            Parameter datesParameter,
            Parameter columnParameter,
            Parameter rowParameter,
            DataTable<String[]> dataTable,
            double intervalWidth,
            List<String> virusLocationStatisticList) {

        super(ANTIGENIC_LIKELIHOOD);

        List<String> strainNames = new ArrayList<String>();
        Map<String, Double> strainDateMap = new HashMap<String, Double>();

        this.intervalWidth = intervalWidth;
        boolean useIntervals = USE_INTERVALS && intervalWidth > 0.0;

        int thresholdCount = 0;

        for (int i = 0; i < dataTable.getRowCount(); i++) {
            String[] values = dataTable.getRow(i);
            int column = columnLabels.indexOf(values[COLUMN_LABEL]);
            if (column == -1) {
                columnLabels.add(values[0]);
                column = columnLabels.size() - 1;
            }

            int columnStrain = -1;
            if (strainTaxa != null) {
                columnStrain = strainTaxa.getTaxonIndex(values[SERUM_STRAIN]);
            } else {
                columnStrain = strainNames.indexOf(values[SERUM_STRAIN]);
                if (columnStrain == -1) {
                    strainNames.add(values[SERUM_STRAIN]);

                    Double date = Double.parseDouble(values[SERUM_DATE]);
                    strainDateMap.put(values[SERUM_STRAIN], date);

                    columnStrain = strainNames.size() - 1;
                }
            }

            if (columnStrain == -1) {
                throw new IllegalArgumentException("Error reading data table: Unrecognized serum strain name, " + values[SERUM_STRAIN] + ", in row " + (i+1));
            }

            int row = rowLabels.indexOf(values[ROW_LABEL]);
            if (row == -1) {
                rowLabels.add(values[ROW_LABEL]);
                row = rowLabels.size() - 1;
            }

            int rowStrain = -1;
            if (strainTaxa != null) {
                rowStrain = strainTaxa.getTaxonIndex(values[VIRUS_STRAIN]);
            } else {
                rowStrain = strainNames.indexOf(values[VIRUS_STRAIN]);
                if (rowStrain == -1) {
                    strainNames.add(values[VIRUS_STRAIN]);

                    Double date = Double.parseDouble(values[VIRUS_DATE]);
                    strainDateMap.put(values[VIRUS_STRAIN], date);

                    rowStrain = strainNames.size() - 1;
                }
            }
            if (rowStrain == -1) {
                throw new IllegalArgumentException("Error reading data table: Unrecognized virus strain name, " + values[VIRUS_STRAIN] + ", in row " + (i+1));
            }

            boolean isThreshold = false;
            double rawTitre = Double.NaN;
            if (values[TITRE].length() > 0) {
                try {
                    rawTitre = Double.parseDouble(values[TITRE]);
                } catch (NumberFormatException nfe) {
                    // check if threshold below
                    if (values[TITRE].contains("<")) {
                        rawTitre = Double.parseDouble(values[TITRE].replace("<",""));
                        isThreshold = true;
                        thresholdCount++;
                    }
                    // check if threshold above
                    if (values[TITRE].contains(">")) {
                        throw new IllegalArgumentException("Error in measurement: unsupported greater than threshold at row " + (i+1));
                    }
                }
            }

            MeasurementType type = (isThreshold ? MeasurementType.THRESHOLD : (useIntervals ? MeasurementType.INTERVAL : MeasurementType.POINT));
            Measurement measurement = new Measurement(column, columnStrain, row, rowStrain, type, rawTitre);

            if (USE_THRESHOLDS || !isThreshold) {
                measurements.add(measurement);
            }

        }

        double[] maxColumnTitre = new double[columnLabels.size()];
        double[] maxRowTitre = new double[rowLabels.size()];
        for (Measurement measurement : measurements) {
            double titre = measurement.log2Titre;
            if (Double.isNaN(titre)) {
                titre = measurement.log2Titre;
            }
            if (titre > maxColumnTitre[measurement.column]) {
                maxColumnTitre[measurement.column] = titre;
            }
            if (titre > maxRowTitre[measurement.row]) {
                maxRowTitre[measurement.row] = titre;
            }
        }

        if (strainTaxa != null) {
            this.strains = strainTaxa;

            // fill in the strain name array for local use
            for (int i = 0; i < strains.getTaxonCount(); i++) {
                strainNames.add(strains.getTaxon(i).getId());
            }

        } else {
            Taxa taxa = new Taxa();
            for (String strain : strainNames) {
                taxa.addTaxon(new Taxon(strain));
            }
            this.strains = taxa;
        }

        this.mdsDimension = mdsDimension;
        this.mdsPrecisionParameter = mdsPrecisionParameter;
        addVariable(mdsPrecisionParameter);

        this.locationsParameter = locationsParameter;
        setupLocationsParameter(this.locationsParameter, strainNames);

        setupInitialLocations(strainNames, strainDateMap);

        this.tipTraitsParameter = tipTraitsParameter;
        if (tipTraitsParameter != null) {
            setupTipTraitsParameter(this.tipTraitsParameter, strainNames);
        }

        if (datesParameter != null) {
            // this parameter is not used in this class but is setup to be used in other classes
            setupDatesParameter(datesParameter, strainNames, strainDateMap);
        }

        this.columnEffectsParameter = setupColumnEffectsParameter(columnParameter, maxColumnTitre);

        this.rowEffectsParameter = setupRowEffectsParameter(rowParameter, maxRowTitre);

        StringBuilder sb = new StringBuilder();
        sb.append("\tAntigenicLikelihood:\n");
        sb.append("\t\t" + this.strains.getTaxonCount() + " strains\n");
        sb.append("\t\t" + columnLabels.size() + " unique columns\n");
        sb.append("\t\t" + rowLabels.size() + " unique rows\n");
        sb.append("\t\t" + measurements.size() + " assay measurements\n");
        if (USE_THRESHOLDS) {
            sb.append("\t\t" + thresholdCount + " thresholded measurements\n");
        }
        if (useIntervals) {
            sb.append("\n\t\tAssuming a log 2 measurement interval width of " + intervalWidth + "\n");
        }
        Logger.getLogger("dr.evomodel").info(sb.toString());

        locationChanged = new boolean[this.locationsParameter.getParameterCount()];
        logLikelihoods = new double[measurements.size()];
        storedLogLikelihoods = new double[measurements.size()];

        makeDirty();
    }

    private Parameter setupRowEffectsParameter(Parameter rowParameter, double[] maxRowTitre) {
        // If no row parameter is given, then we will only use the column effects
        if (rowParameter != null) {
            rowParameter.addBounds(new Parameter.DefaultBounds(Double.MAX_VALUE, 0.0, 1));
            rowParameter.setDimension(rowLabels.size());
            addVariable(rowParameter);
            String[] labelArray = new String[rowLabels.size()];
            rowLabels.toArray(labelArray);
            rowParameter.setDimensionNames(labelArray);
            for (int i = 0; i < maxRowTitre.length; i++) {
                rowParameter.setParameterValueQuietly(i, maxRowTitre[i]);
            }
        }
        return rowParameter;
    }

    private Parameter setupColumnEffectsParameter(Parameter columnParameter, double[] maxColumnTitre) {
        // If no column parameter is given, make one to hold maximum values for scaling titres...
        if (columnParameter == null) {
            columnParameter = new Parameter.Default("columnEffects");
        } else {
            columnParameter.addBounds(new Parameter.DefaultBounds(Double.MAX_VALUE, 0.0, 1));
            addVariable(columnParameter);
        }

        columnParameter.setDimension(columnLabels.size());
        String[] labelArray = new String[columnLabels.size()];
        columnLabels.toArray(labelArray);
        columnParameter.setDimensionNames(labelArray);
        for (int i = 0; i < maxColumnTitre.length; i++) {
            columnParameter.setParameterValueQuietly(i, maxColumnTitre[i]);
        }

        return columnParameter;
    }

    protected void setupLocationsParameter(MatrixParameter locationsParameter, List<String> strains) {
        locationsParameter.setColumnDimension(mdsDimension);
        locationsParameter.setRowDimension(strains.size());
        for (int i = 0; i < strains.size(); i++) {
            locationsParameter.getParameter(i).setId(strains.get(i));
        }
        addVariable(this.locationsParameter);
    }

    private void setupDatesParameter(Parameter datesParameter, List<String> strainNames, Map<String, Double> strainDateMap) {
        datesParameter.setDimension(strainNames.size());
        String[] labelArray = new String[strainNames.size()];
        strainNames.toArray(labelArray);
        datesParameter.setDimensionNames(labelArray);
        for (int i = 0; i < strainNames.size(); i++) {
            Double date = strainDateMap.get(strainNames.get(i));
            if (date == null) {
                throw new IllegalArgumentException("Date missing for strain: " + strainNames.get(i));
            }
            datesParameter.setParameterValue(i, date);
        }
    }

    private void setupTipTraitsParameter(CompoundParameter tipTraitsParameter, List<String> strainNames) {
        tipIndices = new int[strainNames.size()];

        for (int i = 0; i < strainNames.size(); i++) {
            tipIndices[i] = -1;
        }

        for (int i = 0; i < tipTraitsParameter.getParameterCount(); i++) {
            Parameter tip = tipTraitsParameter.getParameter(i);
            String label = tip.getParameterName();
            int index = findStrain(label, strainNames);
            if (index != -1) {
                if (tipIndices[index] != -1) {
                    throw new IllegalArgumentException("Duplicated tip name: " + label);
                }

                tipIndices[index] = i;

                Parameter location = locationsParameter.getParameter(index);
                for (int dim = 0; dim < mdsDimension; dim++) {
                    tip.setParameterValueQuietly(dim, location.getParameterValue(dim));
                }
            } else {
                throw new IllegalArgumentException("Unmatched tip name in assay data: " + label);
            }
        }
        // we are only setting this parameter not listening to it:
//        addVariable(this.tipTraitsParameter);
    }

    private final int findStrain(String label, List<String> strainNames) {
        int index = 0;
        for (String strainName : strainNames) {
            if (label.startsWith(strainName)) {
                return index;
            }

            index ++;
        }
        return -1;
    }

    private void setupInitialLocations(List<String> strainNames, Map<String,Double> strainDateMap) {
        double earliestDate = Double.POSITIVE_INFINITY;
        for (double date : strainDateMap.values()) {
            if (earliestDate > date) {
                earliestDate = date;
            }
        }

        for (int i = 0; i < locationsParameter.getParameterCount(); i++) {
            double date = (double) strainDateMap.get(strainNames.get(i));
            double diff = (date-earliestDate);
            locationsParameter.getParameter(i).setParameterValueQuietly(0, diff + MathUtils.nextGaussian());

            for (int j = 1; j < mdsDimension; j++) {
                double r = MathUtils.nextGaussian();
                locationsParameter.getParameter(i).setParameterValueQuietly(j, r);
            }
        }
    }

    @Override
    protected void handleModelChangedEvent(Model model, Object object, int index) {
    }

    @Override
    protected void handleVariableChangedEvent(Variable variable, int index, Variable.ChangeType type) {
        if (variable == locationsParameter) {
            int loc = index / mdsDimension;
            locationChanged[loc] = true;
            if (tipTraitsParameter != null && tipIndices[loc] != -1) {
                Parameter location = locationsParameter.getParameter(loc);
                Parameter tip = tipTraitsParameter.getParameter(tipIndices[loc]);<commit_msg>Sets all the tip locations now but something is still wrong.<commit_after>package dr.evomodel.antigenic;

import dr.evolution.util.*;
import dr.inference.model.*;
import dr.math.MathUtils;
import dr.math.distributions.NormalDistribution;
import dr.util.*;
import dr.xml.*;

import java.io.*;
import java.util.*;
import java.util.logging.Logger;

/**
 * @author <NAME>
 * @author <NAME>
 * @author <NAME>
 * @version $Id$
 */
public class AntigenicLikelihood extends AbstractModelLikelihood implements Citable {
    private static final boolean CHECK_INFINITE = false;
    private static final boolean USE_THRESHOLDS = true;
    private static final boolean USE_INTERVALS = true;

    public final static String ANTIGENIC_LIKELIHOOD = "antigenicLikelihood";

    // column indices in table
    private static final int COLUMN_LABEL = 0;
    private static final int SERUM_STRAIN = 2;
    private static final int ROW_LABEL = 1;
    private static final int VIRUS_STRAIN = 3;
    private static final int SERUM_DATE = 4;
    private static final int VIRUS_DATE = 5;
    private static final int TITRE = 6;

    public enum MeasurementType {
        INTERVAL,
        POINT,
        THRESHOLD,
        MISSING
    }

    public AntigenicLikelihood(
            int mdsDimension,
            Parameter mdsPrecisionParameter,
            TaxonList strainTaxa,
            MatrixParameter locationsParameter,
            CompoundParameter tipTraitsParameter,
            Parameter datesParameter,
            Parameter columnParameter,
            Parameter rowParameter,
            DataTable<String[]> dataTable,
            double intervalWidth,
            List<String> virusLocationStatisticList) {

        super(ANTIGENIC_LIKELIHOOD);

        List<String> strainNames = new ArrayList<String>();
        Map<String, Double> strainDateMap = new HashMap<String, Double>();

        this.intervalWidth = intervalWidth;
        boolean useIntervals = USE_INTERVALS && intervalWidth > 0.0;

        int thresholdCount = 0;

        for (int i = 0; i < dataTable.getRowCount(); i++) {
            String[] values = dataTable.getRow(i);
            int column = columnLabels.indexOf(values[COLUMN_LABEL]);
            if (column == -1) {
                columnLabels.add(values[0]);
                column = columnLabels.size() - 1;
            }

            int columnStrain = -1;
            if (strainTaxa != null) {
                columnStrain = strainTaxa.getTaxonIndex(values[SERUM_STRAIN]);
            } else {
                columnStrain = strainNames.indexOf(values[SERUM_STRAIN]);
                if (columnStrain == -1) {
                    strainNames.add(values[SERUM_STRAIN]);

                    Double date = Double.parseDouble(values[SERUM_DATE]);
                    strainDateMap.put(values[SERUM_STRAIN], date);

                    columnStrain = strainNames.size() - 1;
                }
            }

            if (columnStrain == -1) {
                throw new IllegalArgumentException("Error reading data table: Unrecognized serum strain name, " + values[SERUM_STRAIN] + ", in row " + (i+1));
            }

            int row = rowLabels.indexOf(values[ROW_LABEL]);
            if (row == -1) {
                rowLabels.add(values[ROW_LABEL]);
                row = rowLabels.size() - 1;
            }

            int rowStrain = -1;
            if (strainTaxa != null) {
                rowStrain = strainTaxa.getTaxonIndex(values[VIRUS_STRAIN]);
            } else {
                rowStrain = strainNames.indexOf(values[VIRUS_STRAIN]);
                if (rowStrain == -1) {
                    strainNames.add(values[VIRUS_STRAIN]);

                    Double date = Double.parseDouble(values[VIRUS_DATE]);
                    strainDateMap.put(values[VIRUS_STRAIN], date);

                    rowStrain = strainNames.size() - 1;
                }
            }
            if (rowStrain == -1) {
                throw new IllegalArgumentException("Error reading data table: Unrecognized virus strain name, " + values[VIRUS_STRAIN] + ", in row " + (i+1));
            }

            boolean isThreshold = false;
            double rawTitre = Double.NaN;
            if (values[TITRE].length() > 0) {
                try {
                    rawTitre = Double.parseDouble(values[TITRE]);
                } catch (NumberFormatException nfe) {
                    // check if threshold below
                    if (values[TITRE].contains("<")) {
                        rawTitre = Double.parseDouble(values[TITRE].replace("<",""));
                        isThreshold = true;
                        thresholdCount++;
                    }
                    // check if threshold above
                    if (values[TITRE].contains(">")) {
                        throw new IllegalArgumentException("Error in measurement: unsupported greater than threshold at row " + (i+1));
                    }
                }
            }

            MeasurementType type = (isThreshold ? MeasurementType.THRESHOLD : (useIntervals ? MeasurementType.INTERVAL : MeasurementType.POINT));
            Measurement measurement = new Measurement(column, columnStrain, row, rowStrain, type, rawTitre);

            if (USE_THRESHOLDS || !isThreshold) {
                measurements.add(measurement);
            }

        }

        double[] maxColumnTitre = new double[columnLabels.size()];
        double[] maxRowTitre = new double[rowLabels.size()];
        for (Measurement measurement : measurements) {
            double titre = measurement.log2Titre;
            if (Double.isNaN(titre)) {
                titre = measurement.log2Titre;
            }
            if (titre > maxColumnTitre[measurement.column]) {
                maxColumnTitre[measurement.column] = titre;
            }
            if (titre > maxRowTitre[measurement.row]) {
                maxRowTitre[measurement.row] = titre;
            }
        }

        if (strainTaxa != null) {
            this.strains = strainTaxa;

            // fill in the strain name array for local use
            for (int i = 0; i < strains.getTaxonCount(); i++) {
                strainNames.add(strains.getTaxon(i).getId());
            }

        } else {
            Taxa taxa = new Taxa();
            for (String strain : strainNames) {
                taxa.addTaxon(new Taxon(strain));
            }
            this.strains = taxa;
        }

        this.mdsDimension = mdsDimension;
        this.mdsPrecisionParameter = mdsPrecisionParameter;
        addVariable(mdsPrecisionParameter);

        this.locationsParameter = locationsParameter;
        setupLocationsParameter(this.locationsParameter, strainNames);

        this.tipTraitsParameter = tipTraitsParameter;
        if (tipTraitsParameter != null) {
            setupTipTraitsParameter(this.tipTraitsParameter, strainNames);
        }

        if (datesParameter != null) {
            // this parameter is not used in this class but is setup to be used in other classes
            setupDatesParameter(datesParameter, strainNames, strainDateMap);
        }

        this.columnEffectsParameter = setupColumnEffectsParameter(columnParameter, maxColumnTitre);

        this.rowEffectsParameter = setupRowEffectsParameter(rowParameter, maxRowTitre);

        StringBuilder sb = new StringBuilder();
        sb.append("\tAntigenicLikelihood:\n");
        sb.append("\t\t" + this.strains.getTaxonCount() + " strains\n");
        sb.append("\t\t" + columnLabels.size() + " unique columns\n");
        sb.append("\t\t" + rowLabels.size() + " unique rows\n");
        sb.append("\t\t" + measurements.size() + " assay measurements\n");
        if (USE_THRESHOLDS) {
            sb.append("\t\t" + thresholdCount + " thresholded measurements\n");
        }
        if (useIntervals) {
            sb.append("\n\t\tAssuming a log 2 measurement interval width of " + intervalWidth + "\n");
        }
        Logger.getLogger("dr.evomodel").info(sb.toString());

        locationChanged = new boolean[this.locationsParameter.getParameterCount()];
        logLikelihoods = new double[measurements.size()];
        storedLogLikelihoods = new double[measurements.size()];

        setupInitialLocations(strainNames, strainDateMap);

        makeDirty();
    }

    private Parameter setupRowEffectsParameter(Parameter rowParameter, double[] maxRowTitre) {
        // If no row parameter is given, then we will only use the column effects
        if (rowParameter != null) {
            rowParameter.addBounds(new Parameter.DefaultBounds(Double.MAX_VALUE, 0.0, 1));
            rowParameter.setDimension(rowLabels.size());
            addVariable(rowParameter);
            String[] labelArray = new String[rowLabels.size()];
            rowLabels.toArray(labelArray);
            rowParameter.setDimensionNames(labelArray);
            for (int i = 0; i < maxRowTitre.length; i++) {
                rowParameter.setParameterValueQuietly(i, maxRowTitre[i]);
            }
        }
        return rowParameter;
    }

    private Parameter setupColumnEffectsParameter(Parameter columnParameter, double[] maxColumnTitre) {
        // If no column parameter is given, make one to hold maximum values for scaling titres...
        if (columnParameter == null) {
            columnParameter = new Parameter.Default("columnEffects");
        } else {
            columnParameter.addBounds(new Parameter.DefaultBounds(Double.MAX_VALUE, 0.0, 1));
            addVariable(columnParameter);
        }

        columnParameter.setDimension(columnLabels.size());
        String[] labelArray = new String[columnLabels.size()];
        columnLabels.toArray(labelArray);
        columnParameter.setDimensionNames(labelArray);
        for (int i = 0; i < maxColumnTitre.length; i++) {
            columnParameter.setParameterValueQuietly(i, maxColumnTitre[i]);
        }

        return columnParameter;
    }

    protected void setupLocationsParameter(MatrixParameter locationsParameter, List<String> strains) {
        locationsParameter.setColumnDimension(mdsDimension);
        locationsParameter.setRowDimension(strains.size());
        for (int i = 0; i < strains.size(); i++) {
            locationsParameter.getParameter(i).setId(strains.get(i));
        }
        addVariable(this.locationsParameter);
    }

    private void setupDatesParameter(Parameter datesParameter, List<String> strainNames, Map<String, Double> strainDateMap) {
        datesParameter.setDimension(strainNames.size());
        String[] labelArray = new String[strainNames.size()];
        strainNames.toArray(labelArray);
        datesParameter.setDimensionNames(labelArray);
        for (int i = 0; i < strainNames.size(); i++) {
            Double date = strainDateMap.get(strainNames.get(i));
            if (date == null) {
                throw new IllegalArgumentException("Date missing for strain: " + strainNames.get(i));
            }
            datesParameter.setParameterValue(i, date);
        }
    }

    private void setupTipTraitsParameter(CompoundParameter tipTraitsParameter, List<String> strainNames) {
        tipIndices = new int[strainNames.size()];

        for (int i = 0; i < strainNames.size(); i++) {
            tipIndices[i] = -1;
        }

        for (int i = 0; i < tipTraitsParameter.getParameterCount(); i++) {
            Parameter tip = tipTraitsParameter.getParameter(i);
            String label = tip.getParameterName();
            int index = findStrain(label, strainNames);
            if (index != -1) {
                if (tipIndices[index] != -1) {
                    throw new IllegalArgumentException("Duplicated tip name: " + label);
                }

                tipIndices[index] = i;

                // rather than setting these here, we set them when the locations are set so the changes propagate
                // through to the diffusion model.
//                Parameter location = locationsParameter.getParameter(index);
//                for (int dim = 0; dim < mdsDimension; dim++) {
//                    tip.setParameterValue(dim, location.getParameterValue(dim));
//                }
            } else {
                throw new IllegalArgumentException("Unmatched tip name in assay data: " + label);
            }
        }
        // we are only setting this parameter not listening to it:
//        addVariable(this.tipTraitsParameter);
    }

    private final int findStrain(String label, List<String> strainNames) {
        int index = 0;
        for (String strainName : strainNames) {
            if (label.startsWith(strainName)) {
                return index;
            }

            index ++;
        }
        return -1;
    }

    private void setupInitialLocations(List<String> strainNames, Map<String,Double> strainDateMap) {
        double earliestDate = Double.POSITIVE_INFINITY;
        for (double date : strainDateMap.values()) {
            if (earliestDate > date) {
                earliestDate = date;
            }
        }

        for (int i = 0; i < locationsParameter.getParameterCount(); i++) {
            double date = (double) strainDateMap.get(strainNames.get(i));
            double diff = (date-earliestDate);
            locationsParameter.getParameter(i).setParameterValue(0, diff + MathUtils.nextGaussian());

            for (int j = 1; j < mdsDimension; j++) {
                double r = MathUtils.nextGaussian();
                locationsParameter.getParameter(i).setParameterValue(j, r);
            }
        }
    }

    @Override
    protected void handleModelChangedEvent(Model model, Object object, int index) {
    }

    @Override
    protected void handleVariableChangedEvent(Variable variable, int index, Variable.ChangeType type) {
        if (variable == locationsParameter) {
            int loc = index / mdsDimension;
            locationChanged[loc] = true;
            if (tipTraitsParameter != null && tipIndices[loc] != -1) {
                Parameter location = locationsParameter.getParameter(loc);
                Parameter tip = tipTraitsParameter.getParameter(tipIndices[loc]);
{'fraction_non_alphanumeric': 0.10094063992018813, 'fraction_numerical': 0.0038480724007696147, 'mean_word_length': 2.103726639389583, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3395456', 'n_tokens_mistral': 7607, 'n_tokens_neox': 7291, 'n_words': 1693}