diff --git a/gemma-cli/src/main/java/ubic/gemma/core/util/EntityLocatorImpl.java b/gemma-cli/src/main/java/ubic/gemma/core/util/EntityLocatorImpl.java index a133723676..7b961c72a8 100644 --- a/gemma-cli/src/main/java/ubic/gemma/core/util/EntityLocatorImpl.java +++ b/gemma-cli/src/main/java/ubic/gemma/core/util/EntityLocatorImpl.java @@ -221,20 +221,19 @@ public CellTypeAssignment locateCellTypeAssignment( ExpressionExperiment express Assert.isTrue( StringUtils.isNotBlank( cta ), "Cell type assignment name must not be blank." ); cta = StringUtils.strip( cta ); try { - Optional c = singleCellExpressionExperimentService.getCellTypeAssignment( expressionExperiment, qt, Long.parseLong( cta ) ); - if ( c.isPresent() ) { - return c.get(); + CellTypeAssignment c = singleCellExpressionExperimentService.getCellTypeAssignment( expressionExperiment, qt, Long.parseLong( cta ) ); + if ( c != null ) { + return c; } } catch ( NumberFormatException e ) { // ignore } String finalCta = cta; - return singleCellExpressionExperimentService.getCellTypeAssignment( expressionExperiment, qt, cta ) - .orElseThrow( () -> { - List possibleValues = singleCellExpressionExperimentService.getCellTypeAssignments( expressionExperiment, qt ); - return new NullPointerException( "Could not locate any cell type assignment with identifier or name matching " + finalCta + "." + formatPossibleValues( possibleValues, true ) ); - } ); + return requireNonNull( singleCellExpressionExperimentService.getCellTypeAssignment( expressionExperiment, qt, cta ), () -> { + List possibleValues = singleCellExpressionExperimentService.getCellTypeAssignments( expressionExperiment, qt ); + return "Could not locate any cell type assignment with identifier or name matching " + finalCta + "." + formatPossibleValues( possibleValues, true ); + } ); } @Override diff --git a/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/QuantitationTypeConversionUtils.java b/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/QuantitationTypeConversionUtils.java index d756ad210b..54e2f28ee0 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/QuantitationTypeConversionUtils.java +++ b/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/QuantitationTypeConversionUtils.java @@ -21,6 +21,7 @@ import cern.colt.matrix.DoubleMatrix1D; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; import ubic.basecode.dataStructure.matrix.DoubleMatrix; import ubic.basecode.math.MatrixStats; import ubic.gemma.core.analysis.preprocess.detect.InferredQuantitationMismatchException; @@ -30,11 +31,15 @@ import ubic.gemma.core.analysis.preprocess.filter.ExpressionExperimentFilter; import ubic.gemma.core.datastructure.matrix.ExpressionDataDoubleMatrix; import ubic.gemma.model.common.quantitationtype.*; +import ubic.gemma.model.expression.bioAssayData.DataVector; import ubic.gemma.model.expression.biomaterial.BioMaterial; import ubic.gemma.model.expression.designElement.CompositeSequence; import javax.annotation.CheckReturnValue; -import java.util.List; +import java.beans.PropertyDescriptor; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; import static ubic.gemma.core.analysis.preprocess.detect.QuantitationTypeDetectionUtils.detectSuspiciousValues; @@ -278,4 +283,50 @@ private static boolean isHeterogeneous( ExpressionDataDoubleMatrix expressionDat return false; } + /** + * Convert a collection of vectors. + * @param createQtFunc a function to create a converted {@link QuantitationType} + * @param doToVector a consumer to post-process the created vector (first argument) given the original vector + * (second argument) + * @param vectorType the type of vector to produce + */ + public static Collection convertVectors( Collection vectors, Function createQtFunc, BiConsumer doToVector, Class vectorType ) { + ArrayList result = new ArrayList<>( vectors.size() ); + Map convertedQts = new HashMap<>(); + String[] ignoredProperties = getDataVectorIgnoredProperties( vectorType ); + for ( T vector : vectors ) { + QuantitationType convertedQt = convertedQts.computeIfAbsent( vector.getQuantitationType(), createQtFunc ); + result.add( createVector( vector, vectorType, convertedQt, doToVector, ignoredProperties ) ); + } + return result; + } + + + /** + * Convert a single vector. + */ + public static T convertVector( T vector, Function createQtFunc, BiConsumer doToVector, Class vectorType ) { + return createVector( vector, vectorType, createQtFunc.apply( vector.getQuantitationType() ), doToVector, getDataVectorIgnoredProperties( vectorType ) ); + } + + private static T createVector( T vector, Class vectorType, QuantitationType convertedQt, BiConsumer doToVector, String[] ignoredProperties ) { + T convertedVector = BeanUtils.instantiate( vectorType ); + BeanUtils.copyProperties( vector, convertedVector, ignoredProperties ); + convertedVector.setQuantitationType( convertedQt ); + doToVector.accept( convertedVector, vector ); + return convertedVector; + } + + /** + * List of properties to copy over when converting a vector to a different QT. + */ + private static String[] getDataVectorIgnoredProperties( Class vectorType ) { + List ignoredPropertiesList = new ArrayList<>(); + for ( PropertyDescriptor pd : BeanUtils.getPropertyDescriptors( vectorType ) ) { + if ( pd.getName().equals( "quantitationType" ) || ( pd.getName().startsWith( "data" ) && !pd.getName().equals( "dataIndices" ) ) ) { + ignoredPropertiesList.add( pd.getName() ); + } + } + return ignoredPropertiesList.toArray( new String[0] ); + } } diff --git a/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/RepresentationConversionUtils.java b/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/RepresentationConversionUtils.java index 33de9e3077..d4aebd2872 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/RepresentationConversionUtils.java +++ b/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/RepresentationConversionUtils.java @@ -1,18 +1,16 @@ package ubic.gemma.core.analysis.preprocess.convert; import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.BeanUtils; import ubic.gemma.model.common.quantitationtype.PrimitiveType; import ubic.gemma.model.common.quantitationtype.QuantitationType; import ubic.gemma.model.expression.bioAssayData.DataVector; -import java.beans.PropertyDescriptor; -import java.util.*; +import java.util.Collection; import static ubic.gemma.persistence.util.ByteArrayUtils.doubleArrayToBytes; /** - * Convert {@link ubic.gemma.model.expression.bioAssayData.DataVector} from different representations. + * Convert {@link DataVector} to different {@link PrimitiveType}. * @author poirigui */ public class RepresentationConversionUtils { @@ -21,51 +19,21 @@ public class RepresentationConversionUtils { * Convert a collection of vectors to a desired representation. */ public static Collection convertVectors( Collection vectors, PrimitiveType toRepresentation, Class vectorType ) { - ArrayList result = new ArrayList<>( vectors.size() ); - Map convertedQts = new HashMap<>(); - List ignoredPropertiesList = new ArrayList<>(); - for ( PropertyDescriptor pd : BeanUtils.getPropertyDescriptors( vectorType ) ) { - if ( pd.getName().equals( "quantitationType" ) || ( pd.getName().startsWith( "data" ) && !pd.getName().equals( "dataIndices" ) ) ) { - ignoredPropertiesList.add( pd.getName() ); - } - } - String[] ignoredProperties = ignoredPropertiesList.toArray( new String[0] ); - for ( T vector : vectors ) { - QuantitationType qt = vector.getQuantitationType(); - QuantitationType convertedQt = convertedQts.computeIfAbsent( qt, qt2 -> { - QuantitationType quantitationType = QuantitationType.Factory.newInstance( qt2 ); - String description; - if ( StringUtils.isNotBlank( qt.getDescription() ) ) { - description = StringUtils.appendIfMissing( StringUtils.strip( qt.getDescription() ), "." ) + " "; - } else { - description = ""; - } - description += "Data was converted from " + qt.getRepresentation() + " to " + toRepresentation + "."; - quantitationType.setDescription( description ); - quantitationType.setRepresentation( toRepresentation ); - return quantitationType; - } ); - T convertedVector = BeanUtils.instantiate( vectorType ); - BeanUtils.copyProperties( vector, convertedVector, ignoredProperties ); - convertedVector.setQuantitationType( convertedQt ); - convertedVector.setData( convertData( vector, toRepresentation ) ); - result.add( convertedVector ); - } - return result; + return QuantitationTypeConversionUtils.convertVectors( vectors, qt -> getConvertedQuantitationType( qt, toRepresentation ), ( vec, origVec ) -> vec.setData( convertData( origVec, toRepresentation ) ), vectorType ); } - /** - * Convert a single vector to a desired representation. - */ - public static T convertVector( T vector, PrimitiveType toRepresentation, Class vectorType ) { - QuantitationType qt = vector.getQuantitationType(); - QuantitationType convertedQt = QuantitationType.Factory.newInstance( qt ); - convertedQt.setRepresentation( toRepresentation ); - T convertedVector = BeanUtils.instantiate( vectorType ); - BeanUtils.copyProperties( vector, convertedVector ); - convertedVector.setQuantitationType( convertedQt ); - convertedVector.setData( convertData( vector, toRepresentation ) ); - return convertedVector; + private static QuantitationType getConvertedQuantitationType( QuantitationType qt, PrimitiveType toRepresentation ) { + QuantitationType quantitationType = QuantitationType.Factory.newInstance( qt ); + String description; + if ( StringUtils.isNotBlank( qt.getDescription() ) ) { + description = StringUtils.appendIfMissing( StringUtils.strip( qt.getDescription() ), "." ) + " "; + } else { + description = ""; + } + description += "Data was converted from " + qt.getRepresentation() + " to " + toRepresentation + "."; + quantitationType.setDescription( description ); + quantitationType.setRepresentation( toRepresentation ); + return quantitationType; } private static byte[] convertData( DataVector vector, PrimitiveType to ) { diff --git a/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/ScaleTypeConversionUtils.java b/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/ScaleTypeConversionUtils.java index a4f9479031..aa4de3bcb4 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/ScaleTypeConversionUtils.java +++ b/gemma-core/src/main/java/ubic/gemma/core/analysis/preprocess/convert/ScaleTypeConversionUtils.java @@ -1,14 +1,18 @@ package ubic.gemma.core.analysis.preprocess.convert; +import org.apache.commons.lang3.StringUtils; +import ubic.gemma.model.common.quantitationtype.PrimitiveType; import ubic.gemma.model.common.quantitationtype.QuantitationType; import ubic.gemma.model.common.quantitationtype.ScaleType; import ubic.gemma.model.common.quantitationtype.StandardQuantitationType; import ubic.gemma.model.expression.bioAssayData.DataVector; +import java.util.Collection; + /** - * Utilities for converting data vectors to different scales and representations. + * Convert {@link DataVector} to different {@link ScaleType}. *

- * For now, all conversions produce doubles + * For now, all conversions produce {@link PrimitiveType#DOUBLE}. * @author poirigui */ public class ScaleTypeConversionUtils { @@ -18,6 +22,25 @@ public class ScaleTypeConversionUtils { private static final ThreadLocal ONE_FLOAT_VALUE = ThreadLocal.withInitial( () -> new float[1] ); private static final ThreadLocal ONE_DOUBLE_VALUE = ThreadLocal.withInitial( () -> new double[1] ); + public static Collection convertVectors( Collection vectors, ScaleType toScale, Class vectorType ) { + return QuantitationTypeConversionUtils.convertVectors( vectors, qt -> getConvertedQuantitationType( qt, toScale ), ( vec, origVec ) -> vec.setDataAsDoubles( convertData( origVec, toScale ) ), vectorType ); + } + + private static QuantitationType getConvertedQuantitationType( QuantitationType qt, ScaleType toScale ) { + QuantitationType quantitationType = QuantitationType.Factory.newInstance( qt ); + String description; + if ( StringUtils.isNotBlank( qt.getDescription() ) ) { + description = StringUtils.appendIfMissing( StringUtils.strip( qt.getDescription() ), "." ) + " "; + } else { + description = ""; + } + description += "Data was converted from " + qt.getScale() + " to " + toScale + "."; + quantitationType.setDescription( description ); + quantitationType.setScale( toScale ); + quantitationType.setRepresentation( PrimitiveType.DOUBLE ); + return quantitationType; + } + /** * Convert a single number. *

@@ -31,19 +54,19 @@ public static double convertScalar( Number val, QuantitationType qt, ScaleType s if ( val instanceof Float ) { float[] vec = ONE_FLOAT_VALUE.get(); vec[0] = val.floatValue(); - return convertVector( vec, qt, scaleType )[0]; + return convertData( vec, qt, scaleType )[0]; } else if ( val instanceof Double ) { double[] vec = ONE_DOUBLE_VALUE.get(); vec[0] = val.doubleValue(); - return convertVector( vec, qt, scaleType )[0]; + return convertData( vec, qt, scaleType )[0]; } else if ( val instanceof Integer ) { int[] vec = ONE_INT_VALUE.get(); vec[0] = val.intValue(); - return convertVector( vec, scaleType )[0]; + return convertData( vec, scaleType )[0]; } else if ( val instanceof Long ) { long[] vec = ONE_LONG_VALUE.get(); vec[0] = val.longValue(); - return convertVector( vec, scaleType )[0]; + return convertData( vec, scaleType )[0]; } else { throw new UnsupportedOperationException( "Cannot convert " + val.getClass().getSimpleName() + " to " + scaleType + " scale." ); } @@ -65,16 +88,16 @@ public static void clearScalarConversionThreadLocalStorage() { * @throws IllegalArgumentException if the conversion is not possible * @throws UnsupportedOperationException if the conversion is not supported */ - public static double[] convertVector( DataVector vec, ScaleType scaleType ) { + public static double[] convertData( DataVector vec, ScaleType scaleType ) { switch ( vec.getQuantitationType().getRepresentation() ) { case FLOAT: - return convertVector( vec.getDataAsFloats(), vec.getQuantitationType(), scaleType ); + return convertData( vec.getDataAsFloats(), vec.getQuantitationType(), scaleType ); case DOUBLE: - return convertVector( vec.getDataAsDoubles(), vec.getQuantitationType(), scaleType ); + return convertData( vec.getDataAsDoubles(), vec.getQuantitationType(), scaleType ); case INT: - return convertVector( vec.getDataAsInts(), scaleType ); + return convertData( vec.getDataAsInts(), scaleType ); case LONG: - return convertVector( vec.getDataAsLongs(), scaleType ); + return convertData( vec.getDataAsLongs(), scaleType ); default: throw new UnsupportedOperationException( "Conversion of " + vec.getQuantitationType().getRepresentation() + " is not supported." ); } @@ -83,15 +106,15 @@ public static double[] convertVector( DataVector vec, ScaleType scaleType ) { /** * Convert a vector of float data to the target scale. */ - public static double[] convertVector( float[] vec, QuantitationType quantitationType, ScaleType scaleType ) { - return convertVector( float2double( vec ), quantitationType.getType(), quantitationType.getScale(), scaleType ); + public static double[] convertData( float[] vec, QuantitationType quantitationType, ScaleType scaleType ) { + return convertData( float2double( vec ), quantitationType.getType(), quantitationType.getScale(), scaleType ); } /** * Convert a vector of double data to the target scale. */ - public static double[] convertVector( double[] vec, QuantitationType quantitationType, ScaleType scaleType ) { - return convertVector( vec, quantitationType.getType(), quantitationType.getScale(), scaleType ); + public static double[] convertData( double[] vec, QuantitationType quantitationType, ScaleType scaleType ) { + return convertData( vec, quantitationType.getType(), quantitationType.getScale(), scaleType ); } /** @@ -99,7 +122,7 @@ public static double[] convertVector( double[] vec, QuantitationType quantitatio *

* The type and scale are assumed to be counts. */ - public static double[] convertVector( int[] vec, ScaleType scaleType ) { + public static double[] convertData( int[] vec, ScaleType scaleType ) { if ( scaleType == ScaleType.LINEAR || scaleType == ScaleType.COUNT ) { return int2double( vec ); } @@ -129,7 +152,7 @@ public static double[] convertVector( int[] vec, ScaleType scaleType ) { *

* The type and scale are assumed to be counts. */ - public static double[] convertVector( long[] vec, ScaleType scaleType ) { + public static double[] convertData( long[] vec, ScaleType scaleType ) { if ( scaleType == ScaleType.LINEAR || scaleType == ScaleType.COUNT ) { return long2double( vec ); } @@ -154,7 +177,7 @@ public static double[] convertVector( long[] vec, ScaleType scaleType ) { return result; } - public static double[] convertVector( double[] vec, StandardQuantitationType fromType, ScaleType fromScale, ScaleType scaleType ) { + public static double[] convertData( double[] vec, StandardQuantitationType fromType, ScaleType fromScale, ScaleType scaleType ) { if ( fromScale == scaleType ) { return vec; } diff --git a/gemma-core/src/main/java/ubic/gemma/core/analysis/singleCell/SingleCellDescriptive.java b/gemma-core/src/main/java/ubic/gemma/core/analysis/singleCell/SingleCellDescriptive.java index 0259a1e5f6..ef94d42f9a 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/analysis/singleCell/SingleCellDescriptive.java +++ b/gemma-core/src/main/java/ubic/gemma/core/analysis/singleCell/SingleCellDescriptive.java @@ -2,6 +2,7 @@ import cern.colt.list.DoubleArrayList; import cern.jet.stat.Descriptive; +import org.springframework.util.Assert; import ubic.basecode.math.DescriptiveWithMissing; import ubic.gemma.core.analysis.stats.DataVectorDescriptive; import ubic.gemma.model.common.quantitationtype.PrimitiveType; @@ -9,6 +10,7 @@ import ubic.gemma.model.common.quantitationtype.ScaleType; import ubic.gemma.model.common.quantitationtype.StandardQuantitationType; import ubic.gemma.model.expression.bioAssay.BioAssay; +import ubic.gemma.model.expression.bioAssayData.CellLevelCharacteristics; import ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVector; import java.util.function.ToDoubleFunction; @@ -30,10 +32,26 @@ public static double[] max( SingleCellExpressionDataVector vector ) { return applyDescriptive( vector, DescriptiveWithMissing::max, "max" ); } + public static double max( SingleCellExpressionDataVector vector, int sampleIndex ) { + return applyDescriptive( vector, sampleIndex, DescriptiveWithMissing::max, "max" ); + } + + public static double max( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + return applyDescriptive( vector, sampleIndex, cellLevelCharacteristics, row, DescriptiveWithMissing::max, "max" ); + } + public static double[] min( SingleCellExpressionDataVector vector ) { return applyDescriptive( vector, DescriptiveWithMissing::min, "min" ); } + public static double min( SingleCellExpressionDataVector vector, int sampleIndex ) { + return applyDescriptive( vector, sampleIndex, DescriptiveWithMissing::min, "min" ); + } + + public static double min( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + return applyDescriptive( vector, sampleIndex, cellLevelCharacteristics, row, DescriptiveWithMissing::min, "min" ); + } + /** * Count the number of values in each assay. *

@@ -223,13 +241,30 @@ private static int[] countCount( SingleCellExpressionDataVector vector, double[] public static int[] countFast( SingleCellExpressionDataVector vector ) { int[] d = new int[vector.getSingleCellDimension().getBioAssays().size()]; for ( int i = 0; i < d.length; i++ ) { - int start = getSampleStart( vector, i, 0 ); - int end = getSampleEnd( vector, i, start ); - d[i] = end - start; + d[i] = countFast( vector, i ); } return d; } + public static int countFast( SingleCellExpressionDataVector vector, int sampleIndex ) { + int start = getSampleStart( vector, sampleIndex, 0 ); + int end = getSampleEnd( vector, sampleIndex, start ); + return end - start; + } + + public static int countFast( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + Assert.isTrue( row >= -1 && row < cellLevelCharacteristics.getNumberOfCharacteristics() ); + int start = getSampleStart( vector, sampleIndex, 0 ); + int end = getSampleEnd( vector, sampleIndex, start ); + int count = 0; + for ( int i = start; i < end; i++ ) { + if ( cellLevelCharacteristics.getIndices()[i] == row ) { + count++; + } + } + return count; + } + /** * Compute the number of cells expressed strictly above the given threshold. * @param threshold a threshold value, assumed to be in the {@link ScaleType} of the vector @@ -373,6 +408,36 @@ private static double[] applyLongDescriptive( SingleCellExpressionDataVector vec return d; } + private static double applyDescriptive( SingleCellExpressionDataVector vector, int sampleIndex, ToDoubleFunction func, String operation ) { + switch ( vector.getQuantitationType().getRepresentation() ) { + case FLOAT: + return func.applyAsDouble( new DoubleArrayList( float2double( getSampleDataAsFloats( vector, sampleIndex ) ) ) ); + case DOUBLE: + return func.applyAsDouble( new DoubleArrayList( getSampleDataAsDoubles( vector, sampleIndex ) ) ); + case INT: + return func.applyAsDouble( new DoubleArrayList( int2double( getSampleDataAsInts( vector, sampleIndex ) ) ) ); + case LONG: + return func.applyAsDouble( new DoubleArrayList( long2double( getSampleDataAsLongs( vector, sampleIndex ) ) ) ); + default: + throw unsupportedRepresentation( vector.getQuantitationType().getRepresentation(), operation ); + } + } + + private static double applyDescriptive( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row, ToDoubleFunction func, String operation ) { + switch ( vector.getQuantitationType().getRepresentation() ) { + case FLOAT: + return func.applyAsDouble( new DoubleArrayList( float2double( getSampleDataAsFloats( vector, sampleIndex, cellLevelCharacteristics, row ) ) ) ); + case DOUBLE: + return func.applyAsDouble( new DoubleArrayList( getSampleDataAsDoubles( vector, sampleIndex, cellLevelCharacteristics, row ) ) ); + case INT: + return func.applyAsDouble( new DoubleArrayList( int2double( getSampleDataAsInts( vector, sampleIndex, cellLevelCharacteristics, row ) ) ) ); + case LONG: + return func.applyAsDouble( new DoubleArrayList( long2double( getSampleDataAsLongs( vector, sampleIndex, cellLevelCharacteristics, row ) ) ) ); + default: + throw unsupportedRepresentation( vector.getQuantitationType().getRepresentation(), operation ); + } + } + public static double[] sum( SingleCellExpressionDataVector vector ) { ScaleType scaleType = vector.getQuantitationType().getScale(); PrimitiveType representation = vector.getQuantitationType().getRepresentation(); @@ -458,6 +523,10 @@ public static double mean( SingleCellExpressionDataVector vector, BioAssay sampl if ( sampleIndex == -1 ) { throw new IllegalArgumentException( "Sample not found in vector" ); } + return mean( vector, sampleIndex ); + } + + public static double mean( SingleCellExpressionDataVector vector, int sampleIndex ) { switch ( vector.getQuantitationType().getRepresentation() ) { case FLOAT: return DataVectorDescriptive.mean( getSampleDataAsFloats( vector, sampleIndex ), vector.getQuantitationType().getScale() ); @@ -472,10 +541,37 @@ public static double mean( SingleCellExpressionDataVector vector, BioAssay sampl } } + public static double mean( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + switch ( vector.getQuantitationType().getRepresentation() ) { + case FLOAT: + return DataVectorDescriptive.mean( getSampleDataAsFloats( vector, sampleIndex, cellLevelCharacteristics, row ), vector.getQuantitationType().getScale() ); + case DOUBLE: + return DataVectorDescriptive.mean( getSampleDataAsDoubles( vector, sampleIndex, cellLevelCharacteristics, row ), vector.getQuantitationType().getScale() ); + case INT: + return DataVectorDescriptive.mean( getSampleDataAsInts( vector, sampleIndex, cellLevelCharacteristics, row ), vector.getQuantitationType().getScale() ); + case LONG: + return DataVectorDescriptive.mean( getSampleDataAsLongs( vector, sampleIndex, cellLevelCharacteristics, row ), vector.getQuantitationType().getScale() ); + default: + throw unsupportedRepresentation( vector.getQuantitationType().getRepresentation(), "mean" ); + } + } + /** * Calculate the median of each assay for a given vector. */ public static double[] median( SingleCellExpressionDataVector vector ) { + return quantile( vector, 0.5 ); + } + + public static double median( SingleCellExpressionDataVector vector, int column ) { + return quantile( vector, 0.5 )[column]; + } + + public static double median( SingleCellExpressionDataVector vector, int column, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + return quantile( vector, column, cellLevelCharacteristics, row, 0.5 ); + } + + public static double[] quantile( SingleCellExpressionDataVector vector, double q ) { PrimitiveType representation = vector.getQuantitationType().getRepresentation(); DoubleArrayList vec = null; double[] d = new double[vector.getSingleCellDimension().getBioAssays().size()]; @@ -504,16 +600,63 @@ public static double[] median( SingleCellExpressionDataVector vector ) { } if ( representation == PrimitiveType.FLOAT || representation == PrimitiveType.DOUBLE ) { // baseCode will sort it for us - d[i] = DescriptiveWithMissing.median( vec ); + d[i] = DescriptiveWithMissing.quantile( vec, q ); } else { // colt does not sort data... :S vec.sort(); - d[i] = Descriptive.median( vec ); + d[i] = Descriptive.quantile( vec, q ); } } return d; } + public static double quantile( SingleCellExpressionDataVector vector, int sampleIndex, double v ) { + return quantile( vector, v )[sampleIndex]; + } + + public static double[] quantile( SingleCellExpressionDataVector vector, CellLevelCharacteristics cellLevelCharacteristics, int row, double q ) { + PrimitiveType representation = vector.getQuantitationType().getRepresentation(); + DoubleArrayList vec = null; + double[] d = new double[vector.getSingleCellDimension().getBioAssays().size()]; + for ( int i = 0; i < d.length; i++ ) { + double[] data; + switch ( representation ) { + case FLOAT: + data = float2double( getSampleDataAsFloats( vector, i, cellLevelCharacteristics, row ) ); + break; + case DOUBLE: + data = getSampleDataAsDoubles( vector, i, cellLevelCharacteristics, row ); + break; + case INT: + data = int2double( getSampleDataAsInts( vector, i, cellLevelCharacteristics, row ) ); + break; + case LONG: + data = long2double( getSampleDataAsLongs( vector, i, cellLevelCharacteristics, row ) ); + break; + default: + throw unsupportedRepresentation( representation, "median" ); + } + if ( vec == null ) { + vec = new DoubleArrayList( data ); + } else { + vec.elements( data ); + } + if ( representation == PrimitiveType.FLOAT || representation == PrimitiveType.DOUBLE ) { + // baseCode will sort it for us + d[i] = DescriptiveWithMissing.quantile( vec, q ); + } else { + // colt does not sort data... :S + vec.sort(); + d[i] = Descriptive.quantile( vec, q ); + } + } + return d; + } + + public static double quantile( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row, double q ) { + return quantile( vector, cellLevelCharacteristics, row, q )[sampleIndex]; + } + public static double[] sampleStandardDeviation( SingleCellExpressionDataVector vector ) { ScaleType scaleType = vector.getQuantitationType().getScale(); PrimitiveType representation = vector.getQuantitationType().getRepresentation(); diff --git a/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/MexMatrixWriter.java b/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/MexMatrixWriter.java index fa0c329a13..d5c711127a 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/MexMatrixWriter.java +++ b/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/MexMatrixWriter.java @@ -10,6 +10,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.springframework.util.Assert; import ubic.basecode.util.FileTools; +import ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils; import ubic.gemma.core.datastructure.matrix.SingleCellExpressionDataDoubleMatrix; import ubic.gemma.core.datastructure.matrix.SingleCellExpressionDataMatrix; import ubic.gemma.core.util.TsvUtils; @@ -34,7 +35,7 @@ import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; -import static ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils.convertVector; +import static ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils.convertData; import static ubic.gemma.core.util.TsvUtils.SUB_DELIMITER; import static ubic.gemma.core.util.TsvUtils.format; import static ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVectorUtils.getSampleEnd; @@ -320,7 +321,7 @@ private void writeDoubleMatrix( SingleCellExpressionDataDoubleMatrix mat, int sa private void writeVector( SingleCellExpressionDataVector vector, int row, MatrixVectorWriter[] writers ) { if ( scaleType != null ) { - writeDoubleVector( vector, convertVector( vector, scaleType ), row, writers ); + writeDoubleVector( vector, ScaleTypeConversionUtils.convertData( vector, scaleType ), row, writers ); } else { switch ( vector.getQuantitationType().getRepresentation() ) { case FLOAT: diff --git a/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/TabularMatrixWriter.java b/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/TabularMatrixWriter.java index 4359b9b10f..e780d89e07 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/TabularMatrixWriter.java +++ b/gemma-core/src/main/java/ubic/gemma/core/datastructure/matrix/io/TabularMatrixWriter.java @@ -4,6 +4,7 @@ import no.uib.cipr.matrix.sparse.CompRowMatrix; import org.apache.commons.lang3.StringUtils; import org.springframework.util.Assert; +import ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils; import ubic.gemma.core.datastructure.matrix.SingleCellExpressionDataDoubleMatrix; import ubic.gemma.core.datastructure.matrix.SingleCellExpressionDataMatrix; import ubic.gemma.core.util.BuildInfo; @@ -28,7 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils.convertVector; +import static ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils.convertData; import static ubic.gemma.core.datastructure.matrix.io.ExpressionDataWriterUtils.appendBaseHeader; import static ubic.gemma.core.datastructure.matrix.io.ExpressionDataWriterUtils.constructSampleName; import static ubic.gemma.core.util.TsvUtils.SUB_DELIMITER; @@ -140,7 +141,7 @@ private void writeHeader( ExpressionExperiment ee, QuantitationType qt, SingleCe private void writeVector( SingleCellExpressionDataVector vector, @Nullable Map> cs2gene, Writer pwriter ) throws IOException { if ( scaleType != null ) { - writeVector( vector.getDesignElement(), cs2gene, vector.getSingleCellDimension(), convertVector( vector, scaleType ), PrimitiveType.DOUBLE, vector.getDataIndices(), pwriter ); + writeVector( vector.getDesignElement(), cs2gene, vector.getSingleCellDimension(), ScaleTypeConversionUtils.convertData( vector, scaleType ), PrimitiveType.DOUBLE, vector.getDataIndices(), pwriter ); } else { switch ( vector.getQuantitationType().getRepresentation() ) { case FLOAT: diff --git a/gemma-core/src/main/java/ubic/gemma/core/visualization/ExpressionDataHeatmap.java b/gemma-core/src/main/java/ubic/gemma/core/visualization/ExpressionDataHeatmap.java index 7811352937..ddb51a936d 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/visualization/ExpressionDataHeatmap.java +++ b/gemma-core/src/main/java/ubic/gemma/core/visualization/ExpressionDataHeatmap.java @@ -5,9 +5,12 @@ import org.jfree.data.general.HeatMapDataset; import org.jfree.data.general.HeatMapUtils; import org.springframework.util.Assert; +import ubic.gemma.model.common.description.Characteristic; +import ubic.gemma.model.common.quantitationtype.QuantitationType; import ubic.gemma.model.expression.bioAssay.BioAssay; import ubic.gemma.model.expression.bioAssayData.BioAssayDimension; import ubic.gemma.model.expression.bioAssayData.BulkExpressionDataVector; +import ubic.gemma.model.expression.bioAssayData.CellLevelCharacteristics; import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.BioAssaySet; import ubic.gemma.model.expression.experiment.ExpressionExperiment; @@ -58,7 +61,7 @@ public static ExpressionDataHeatmap fromVectors( ExpressionExperimentSubSet subS /** * Create a heatmap for a given set of design elements. *

- * in this mode, no image can be generated, but labels can are available. + * in this mode, no image can be generated, but labels are available. * @see #fromVectors(ExpressionExperiment, BioAssayDimension, Slice, List) */ public static ExpressionDataHeatmap fromDesignElements( ExpressionExperiment ee, BioAssayDimension dimension, Slice designElements, @Nullable List genes ) { @@ -82,8 +85,22 @@ public static ExpressionDataHeatmap fromDesignElements( ExpressionExperimentSubS private final Slice vectors; @Nullable private final Slice designElements; + /** + * List of genes to use for display purposes. + */ @Nullable private final List genes; + + /** + * Quantitation type of the single-cell data this heatmap was generated from, if applicable. + */ + @Nullable + private QuantitationType singleCellQuantitationType; + @Nullable + private CellLevelCharacteristics cellLevelCharacteristics; + @Nullable + private Characteristic focusedCellLevelCharacteristic; + private int cellSize = 16; private boolean transpose = false; diff --git a/gemma-core/src/main/java/ubic/gemma/core/visualization/Heatmap.java b/gemma-core/src/main/java/ubic/gemma/core/visualization/Heatmap.java index ca72201a20..eee3008d0a 100644 --- a/gemma-core/src/main/java/ubic/gemma/core/visualization/Heatmap.java +++ b/gemma-core/src/main/java/ubic/gemma/core/visualization/Heatmap.java @@ -5,13 +5,14 @@ import java.awt.*; import java.awt.image.BufferedImage; +import java.io.Serializable; import java.util.List; /** * Minimal interface for a labelled heatmap. * @author poirigui */ -public interface Heatmap { +public interface Heatmap extends Serializable { /** * Borrowed from Heatmap.js diff --git a/gemma-core/src/main/java/ubic/gemma/core/visualization/SingleCellDataBoxplot.java b/gemma-core/src/main/java/ubic/gemma/core/visualization/SingleCellDataBoxplot.java new file mode 100644 index 0000000000..3df0ff367e --- /dev/null +++ b/gemma-core/src/main/java/ubic/gemma/core/visualization/SingleCellDataBoxplot.java @@ -0,0 +1,344 @@ +package ubic.gemma.core.visualization; + +import lombok.Setter; +import org.jfree.chart.annotations.CategoryAnnotation; +import org.jfree.chart.annotations.CategoryPointerAnnotation; +import org.jfree.data.general.DatasetChangeListener; +import org.jfree.data.general.DatasetGroup; +import org.jfree.data.statistics.BoxAndWhiskerCategoryDataset; +import org.springframework.util.Assert; +import ubic.gemma.core.analysis.singleCell.SingleCellDescriptive; +import ubic.gemma.model.common.description.Characteristic; +import ubic.gemma.model.expression.bioAssay.BioAssay; +import ubic.gemma.model.expression.bioAssayData.CellLevelCharacteristics; +import ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVector; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +import static ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVectorUtils.getSampleDataAsDoubles; +import static ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVectorUtils.getSampleStart; + +/** + * Represents a boxplot(s) of single-cell data. + */ +@Setter +public class SingleCellDataBoxplot { + + /** + * Vector to display. + */ + private final SingleCellExpressionDataVector vector; + + /** + * List of assays to display. + *

+ * Must be a subset of the dimension of {@link #vector}. + */ + private List bioAssays; + + /** + * Cell-level characteristics to group single-cell data by. + *

+ * One series of datapoint will be created for each value of the characteristic. + */ + @Nullable + private CellLevelCharacteristics cellLevelCharacteristics; + + /** + * Cell-level characteristic to focus on. + *

+ * Must be one of {@link #cellLevelCharacteristics}. + */ + @Nullable + private Characteristic focusedCellLevelCharacteristic; + + /** + * Whether to show the mean. + */ + private boolean showMean; + + public SingleCellDataBoxplot( SingleCellExpressionDataVector vector ) { + this.vector = vector; + this.bioAssays = vector.getSingleCellDimension().getBioAssays(); + } + + /** + * Create a dataset. + */ + public BoxAndWhiskerCategoryDataset createDataset() { + return new SingleCellBoxAndWhiskerCategoryDataset(); + } + + /** + * Obtain annotations to include in the plot. + */ + public Collection createAnnotations() { + Random random = new Random( 123L ); + int row; + if ( cellLevelCharacteristics != null ) { + row = cellLevelCharacteristics.getCharacteristics().indexOf( focusedCellLevelCharacteristic ); + } else { + row = 0; + } + List annotations = new ArrayList<>(); + for ( BioAssay ba : bioAssays ) { + int sampleStart = getSampleStart( vector, ba ); + double[] data = getSampleDataAsDoubles( vector, ba ); + for ( int i = 0; i < data.length; i++ ) { + if ( cellLevelCharacteristics == null || cellLevelCharacteristics.getIndices()[sampleStart + i] == row ) { + annotations.add( new CategoryPointerAnnotation( "", ba.getName(), data[i], random.nextDouble() * 2 * Math.PI ) ); + } + } + } + return annotations; + } + + /** + * Obtain the number of boxplots that would be displayed. + */ + public int getNumberOfBoxplots() { + int num = bioAssays.size(); + if ( cellLevelCharacteristics != null && focusedCellLevelCharacteristic == null ) { + num *= cellLevelCharacteristics.getCharacteristics().size(); + } + return num; + } + + public void setBioAssays( List bioAssays ) { + Assert.isTrue( new HashSet<>( vector.getSingleCellDimension().getBioAssays() ).containsAll( bioAssays ) ); + this.bioAssays = bioAssays; + } + + public void setCellLevelCharacteristics( @Nullable CellLevelCharacteristics cellLevelCharacteristics ) { + // TODO: the CTAs are lazy-loaded, so we cannot check this + // Assert.isTrue( cellLevelCharacteristics == null || vector.getSingleCellDimension().getCellTypeAssignments().contains( cellLevelCharacteristics ) || vector.getSingleCellDimension().getCellLevelCharacteristics().contains( cellLevelCharacteristics ), + // "The cell-level characteristics must belong to " + vector.getSingleCellDimension() + "." ); + this.cellLevelCharacteristics = cellLevelCharacteristics; + } + + public void setFocusedCellLevelCharacteristic( @Nullable Characteristic focusedCellLevelCharacteristic ) { + Assert.isTrue( focusedCellLevelCharacteristic == null || ( cellLevelCharacteristics != null && cellLevelCharacteristics.getCharacteristics().contains( focusedCellLevelCharacteristic ) ), + "If provided, the focused characteristic must be one of " + cellLevelCharacteristics + "." ); + this.focusedCellLevelCharacteristic = focusedCellLevelCharacteristic; + } + + private class SingleCellBoxAndWhiskerCategoryDataset implements BoxAndWhiskerCategoryDataset { + + private final List columnKeys; + private final Map columnIndex = new HashMap<>(); + private final Map rowIndex = new HashMap<>(); + private final List rowKeys; + + private DatasetGroup group; + + private SingleCellBoxAndWhiskerCategoryDataset() { + int i = 0; + for ( BioAssay ba : bioAssays ) { + columnIndex.put( ba.getName(), i++ ); + } + columnKeys = bioAssays.stream() + .map( BioAssay::getName ) + .collect( Collectors.toList() ); + if ( cellLevelCharacteristics != null ) { + if ( focusedCellLevelCharacteristic != null ) { + rowIndex.put( focusedCellLevelCharacteristic.getValue(), 0 ); + rowKeys = Collections.singletonList( focusedCellLevelCharacteristic.getValue() ); + } else { + int j = 0; + for ( Characteristic c : cellLevelCharacteristics.getCharacteristics() ) { + rowIndex.put( c.getValue(), j++ ); + } + rowKeys = cellLevelCharacteristics.getCharacteristics().stream() + .map( Characteristic::getValue ) + .collect( Collectors.toList() ); + } + } else { + String rowKey = "All cells"; + rowIndex.put( rowKey, 0 ); + rowKeys = Collections.singletonList( rowKey ); + } + } + + @Override + public Number getMeanValue( int row, int column ) { + if ( showMean ) { + if ( cellLevelCharacteristics != null ) { + return SingleCellDescriptive.mean( vector, column, cellLevelCharacteristics, row ); + } else { + return SingleCellDescriptive.mean( vector, column ); + } + } else { + return null; + } + } + + @Override + public Number getMeanValue( Comparable rowKey, Comparable columnKey ) { + return getMeanValue( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getMedianValue( int row, int column ) { + if ( cellLevelCharacteristics != null ) { + return SingleCellDescriptive.median( vector, column, cellLevelCharacteristics, row ); + } + return SingleCellDescriptive.median( vector, column ); + } + + @Override + public Number getMedianValue( Comparable rowKey, Comparable columnKey ) { + return getMedianValue( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getQ1Value( int row, int column ) { + return SingleCellDescriptive.quantile( vector, column, 0.25 ); + } + + @Override + public Number getQ1Value( Comparable rowKey, Comparable columnKey ) { + return getQ1Value( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getQ3Value( int row, int column ) { + if ( cellLevelCharacteristics != null ) { + return SingleCellDescriptive.quantile( vector, column, cellLevelCharacteristics, row, 0.75 ); + } + return SingleCellDescriptive.quantile( vector, column, 0.75 ); + } + + @Override + public Number getQ3Value( Comparable rowKey, Comparable columnKey ) { + return getQ3Value( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getMinRegularValue( int row, int column ) { + if ( cellLevelCharacteristics != null ) { + return SingleCellDescriptive.countFast( vector, column, cellLevelCharacteristics, row ) > 0 ? SingleCellDescriptive.min( vector, column, cellLevelCharacteristics, row ) : null; + } + return SingleCellDescriptive.countFast( vector, column ) > 0 ? SingleCellDescriptive.min( vector, column ) : null; + } + + @Override + public Number getMinRegularValue( Comparable rowKey, Comparable columnKey ) { + return getMinRegularValue( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getMaxRegularValue( int row, int column ) { + if ( cellLevelCharacteristics != null ) { + return SingleCellDescriptive.countFast( vector, column, cellLevelCharacteristics, row ) > 0 ? SingleCellDescriptive.max( vector, column, cellLevelCharacteristics, row ) : null; + } + return SingleCellDescriptive.countFast( vector, column ) > 0 ? SingleCellDescriptive.max( vector, column ) : null; + } + + @Override + public Number getMaxRegularValue( Comparable rowKey, Comparable columnKey ) { + return getMaxRegularValue( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getMinOutlier( int row, int column ) { + return null; + } + + @Override + public Number getMinOutlier( Comparable rowKey, Comparable columnKey ) { + return getMinOutlier( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getMaxOutlier( int row, int column ) { + return null; + } + + @Override + public Number getMaxOutlier( Comparable rowKey, Comparable columnKey ) { + return getMaxOutlier( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public List getOutliers( int row, int column ) { + return Collections.emptyList(); + } + + @Override + public List getOutliers( Comparable rowKey, Comparable columnKey ) { + return getOutliers( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Number getValue( int row, int column ) { + return 0; + } + + @Override + public Number getValue( Comparable rowKey, Comparable columnKey ) { + return getValue( ( int ) rowIndex.get( rowKey ), ( int ) columnIndex.get( columnKey ) ); + } + + @Override + public Comparable getRowKey( int row ) { + return rowKeys.get( row ); + } + + @Override + public int getRowIndex( Comparable key ) { + return rowIndex.getOrDefault( key, -1 ); + } + + @Override + public List getRowKeys() { + return rowKeys; + } + + @Override + public Comparable getColumnKey( int column ) { + return columnKeys.get( column ); + } + + @Override + public int getColumnIndex( Comparable key ) { + return columnIndex.getOrDefault( key, -1 ); + } + + @Override + public List getColumnKeys() { + return columnKeys; + } + + @Override + public int getRowCount() { + return rowKeys.size(); + } + + @Override + public int getColumnCount() { + return columnKeys.size(); + } + + @Override + public void addChangeListener( DatasetChangeListener listener ) { + + } + + @Override + public void removeChangeListener( DatasetChangeListener listener ) { + + } + + @Override + public DatasetGroup getGroup() { + return group; + } + + @Override + public void setGroup( DatasetGroup group ) { + this.group = group; + } + } +} diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssay/BioAssay.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssay/BioAssay.java index 5bcfe3b9ca..eeb7c9d94d 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssay/BioAssay.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssay/BioAssay.java @@ -136,6 +136,11 @@ public class BioAssay extends AbstractDescribable implements SecuredChild { @Nullable private Integer numberOfCellsByDesignElements; + @Override + public int hashCode() { + return super.hashCode(); + } + @Override public boolean equals( Object object ) { if ( this == object ) diff --git a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVectorUtils.java b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVectorUtils.java index 87a91b43d5..b69c250fc6 100644 --- a/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVectorUtils.java +++ b/gemma-core/src/main/java/ubic/gemma/model/expression/bioAssayData/SingleCellExpressionDataVectorUtils.java @@ -1,8 +1,13 @@ package ubic.gemma.model.expression.bioAssayData; +import cern.colt.list.DoubleArrayList; +import cern.colt.list.FloatArrayList; +import cern.colt.list.IntArrayList; +import cern.colt.list.LongArrayList; import org.apache.commons.lang3.time.StopWatch; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; import ubic.gemma.model.expression.bioAssay.BioAssay; import java.util.Arrays; @@ -125,6 +130,68 @@ public static long[] getSampleDataAsLongs( SingleCellExpressionDataVector vector return Arrays.copyOfRange( vector.getDataAsLongs(), start, end ); } + /** + * Obtain the data of a sample. + */ + public static float[] getSampleDataAsFloats( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + Assert.isTrue( row >= -1 && row < cellLevelCharacteristics.getNumberOfCharacteristics() ); + int start = getSampleStart( vector, sampleIndex, 0 ); + int end = getSampleEnd( vector, sampleIndex, start ); + float[] data = vector.getDataAsFloats(); + FloatArrayList arr = new FloatArrayList(); + for ( int i = start; i < end; i++ ) { + if ( cellLevelCharacteristics.getIndices()[i] == row ) { + arr.add( data[i] ); + } + } + return arr.elements(); + } + + /** + * Obtain the data of a sample. + */ + public static double[] getSampleDataAsDoubles( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + Assert.isTrue( row >= -1 && row < cellLevelCharacteristics.getNumberOfCharacteristics() ); + int start = getSampleStart( vector, sampleIndex, 0 ); + int end = getSampleEnd( vector, sampleIndex, start ); + double[] data = vector.getDataAsDoubles(); + DoubleArrayList arr = new DoubleArrayList(); + for ( int i = start; i < end; i++ ) { + if ( cellLevelCharacteristics.getIndices()[i] == row ) { + arr.add( data[i] ); + } + } + return arr.elements(); + } + + public static int[] getSampleDataAsInts( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + Assert.isTrue( row >= -1 && row < cellLevelCharacteristics.getNumberOfCharacteristics() ); + int start = getSampleStart( vector, sampleIndex, 0 ); + int end = getSampleEnd( vector, sampleIndex, start ); + int[] data = vector.getDataAsInts(); + IntArrayList arr = new IntArrayList(); + for ( int i = start; i < end; i++ ) { + if ( cellLevelCharacteristics.getIndices()[i] == row ) { + arr.add( data[i] ); + } + } + return arr.elements(); + } + + public static long[] getSampleDataAsLongs( SingleCellExpressionDataVector vector, int sampleIndex, CellLevelCharacteristics cellLevelCharacteristics, int row ) { + Assert.isTrue( row >= -1 && row < cellLevelCharacteristics.getNumberOfCharacteristics() ); + int start = getSampleStart( vector, sampleIndex, 0 ); + int end = getSampleEnd( vector, sampleIndex, start ); + long[] data = vector.getDataAsLongs(); + LongArrayList arr = new LongArrayList(); + for ( int i = start; i < end; i++ ) { + if ( cellLevelCharacteristics.getIndices()[i] == row ) { + arr.add( data[i] ); + } + } + return arr.elements(); + } + public static Consumer createStreamMonitor( String logCategory, long numVecs ) { Log log = LogFactory.getLog( logCategory ); return new Consumer() { diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java index 940289654e..15f52c0415 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDao.java @@ -15,6 +15,7 @@ import ubic.gemma.model.expression.bioAssay.BioAssay; import ubic.gemma.model.expression.bioAssayData.*; import ubic.gemma.model.expression.biomaterial.BioMaterial; +import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.*; import ubic.gemma.model.genome.Gene; import ubic.gemma.model.genome.Taxon; @@ -593,6 +594,8 @@ Map> getSampleRemovalEvents( */ Stream streamSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quantitationType, int fetchSize, boolean createNewSession ); + SingleCellExpressionDataVector getSingleCellDataVectorWithoutCellIds( ExpressionExperiment ee, QuantitationType quantitationType, CompositeSequence designElement ); + /** * Obtain the number of single-cell vectors for a given QT. */ diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java index cff10171e4..ac5bcdc955 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/ExpressionExperimentDaoImpl.java @@ -2456,6 +2456,24 @@ public Stream streamSingleCellDataVectors( Expre } } + @Override + public SingleCellExpressionDataVector getSingleCellDataVectorWithoutCellIds( ExpressionExperiment ee, QuantitationType quantitationType, CompositeSequence designElement ) { + SingleCellExpressionDataVector vector = ( SingleCellExpressionDataVector ) getSessionFactory().getCurrentSession() + .createQuery( "select scedv.id as id, scedv.data as data, scedv.dataIndices as dataIndices, scedv.originalDesignElement as originalDesignElement from SingleCellExpressionDataVector scedv where scedv.expressionExperiment = :ee and scedv.quantitationType = :qt and scedv.designElement = :de" ) + .setParameter( "ee", ee ) + .setParameter( "qt", quantitationType ) + .setParameter( "de", designElement ) + .setResultTransformer( aliasToBean( SingleCellExpressionDataVector.class ) ) + .uniqueResult(); + if ( vector != null ) { + vector.setExpressionExperiment( ee ); + vector.setDesignElement( designElement ); + vector.setQuantitationType( quantitationType ); + vector.setSingleCellDimension( getSingleCellDimensionWithoutCellIds( ee, quantitationType ) ); + } + return vector; + } + @Override public long getNumberOfSingleCellDataVectors( ExpressionExperiment ee, QuantitationType qt ) { return ( Long ) getSessionFactory().getCurrentSession().createQuery( "select count(scedv) from SingleCellExpressionDataVector scedv " diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentService.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentService.java index c815e4dda2..30425adb9b 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentService.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentService.java @@ -11,6 +11,7 @@ import ubic.gemma.model.expression.bioAssayData.CellTypeAssignment; import ubic.gemma.model.expression.bioAssayData.SingleCellDimension; import ubic.gemma.model.expression.bioAssayData.SingleCellExpressionDataVector; +import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.ExperimentalFactor; import ubic.gemma.model.expression.experiment.ExpressionExperiment; @@ -59,6 +60,14 @@ public interface SingleCellExpressionExperimentService { @Secured({ "IS_AUTHENTICATED_ANONYMOUSLY", "ACL_SECURABLE_READ" }) Stream streamSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quantitationType, int fetchSize ); + /** + * Obtain a single single-cell vector without initializing cell IDs. + * @see #getSingleCellDimensionWithoutCellIds(ExpressionExperiment, QuantitationType) + */ + @Nullable + @Secured({ "IS_AUTHENTICATED_ANONYMOUSLY", "ACL_SECURABLE_READ" }) + SingleCellExpressionDataVector getSingleCellDataVectorWithoutCellIds( ExpressionExperiment ee, QuantitationType quantitationType, CompositeSequence designElement ); + /** * Obtain the number of single-cell vectors for a given quantitation type. */ @@ -195,15 +204,17 @@ int replaceSingleCellDataVectors( ExpressionExperiment ee, QuantitationType quan * Obtain a cell type assignment by ID. * @return that cell type assignmente, or null if none is found */ + @Nullable @Secured({ "IS_AUTHENTICATED_ANONYMOUSLY", "ACL_SECURABLE_READ" }) - Optional getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, Long ctaId ); + CellTypeAssignment getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, Long ctaId ); /** * Obtain a cell type assignment by name. * @return that cell type assignmente, or null if none is found */ + @Nullable @Secured({ "IS_AUTHENTICATED_ANONYMOUSLY", "ACL_SECURABLE_READ" }) - Optional getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, String ctaName ); + CellTypeAssignment getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, String ctaName ); /** * Obtain the preferred cell type labelling from the preferred single-cell vectors. diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceImpl.java b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceImpl.java index b4492bcc1a..2136a1f135 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceImpl.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/service/expression/experiment/SingleCellExpressionExperimentServiceImpl.java @@ -81,6 +81,12 @@ public Stream streamSingleCellDataVectors( Expre return expressionExperimentDao.streamSingleCellDataVectors( ee, quantitationType, fetchSize, true ); } + @Override + @Transactional(readOnly = true) + public SingleCellExpressionDataVector getSingleCellDataVectorWithoutCellIds( ExpressionExperiment ee, QuantitationType quantitationType, CompositeSequence designElement ) { + return expressionExperimentDao.getSingleCellDataVectorWithoutCellIds( ee, quantitationType, designElement ); + } + @Override @Transactional(readOnly = true) public long getNumberOfSingleCellDataVectors( ExpressionExperiment ee, QuantitationType qt ) { @@ -659,14 +665,14 @@ public List getCellTypeAssignments( ExpressionExperiment exp @Override @Transactional(readOnly = true) - public Optional getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, Long ctaId ) { - return Optional.ofNullable( expressionExperimentDao.getCellTypeAssignment( expressionExperiment, qt, ctaId ) ); + public CellTypeAssignment getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, Long ctaId ) { + return expressionExperimentDao.getCellTypeAssignment( expressionExperiment, qt, ctaId ); } @Override @Transactional(readOnly = true) - public Optional getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, String ctaName ) { - return Optional.ofNullable( expressionExperimentDao.getCellTypeAssignment( expressionExperiment, qt, ctaName ) ); + public CellTypeAssignment getCellTypeAssignment( ExpressionExperiment expressionExperiment, QuantitationType qt, String ctaName ) { + return expressionExperimentDao.getCellTypeAssignment( expressionExperiment, qt, ctaName ); } @Override diff --git a/gemma-core/src/main/java/ubic/gemma/persistence/util/EntityUrlBuilder.java b/gemma-core/src/main/java/ubic/gemma/persistence/util/EntityUrlBuilder.java index dcf4c9f001..e478225326 100644 --- a/gemma-core/src/main/java/ubic/gemma/persistence/util/EntityUrlBuilder.java +++ b/gemma-core/src/main/java/ubic/gemma/persistence/util/EntityUrlBuilder.java @@ -7,9 +7,13 @@ import ubic.gemma.model.common.AbstractIdentifiable; import ubic.gemma.model.common.Identifiable; import ubic.gemma.model.common.description.Characteristic; +import ubic.gemma.model.common.quantitationtype.QuantitationType; import ubic.gemma.model.expression.arrayDesign.ArrayDesign; import ubic.gemma.model.expression.bioAssay.BioAssay; +import ubic.gemma.model.expression.bioAssayData.CellLevelCharacteristics; +import ubic.gemma.model.expression.bioAssayData.CellTypeAssignment; import ubic.gemma.model.expression.biomaterial.BioMaterial; +import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.ExperimentalDesign; import ubic.gemma.model.expression.experiment.ExpressionExperiment; import ubic.gemma.model.expression.experiment.ExpressionExperimentSubSet; @@ -17,6 +21,7 @@ import ubic.gemma.model.genome.Gene; import ubic.gemma.model.genome.Taxon; +import javax.annotation.Nullable; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; @@ -216,6 +221,7 @@ public class ExpressionExperimentWebUrl extends WebEntityUrl new NotFoundException( "No cell type assignment with name " + ctaName + " found for " + ee.getShortName() + " and " + qt.getName() + "." ) ); + cta = singleCellExpressionExperimentService.getCellTypeAssignment( ee, qt, ctaName ); + if ( cta == null ) { + throw new NotFoundException( "No cell type assignment with name " + ctaName + " found for " + ee.getShortName() + " and " + qt.getName() + "." ); + } } else { cta = singleCellExpressionExperimentService.getPreferredCellTypeAssignment( ee, qt ) .orElseThrow( () -> new NotFoundException( "No preferred cell type assignment found for " + ee.getShortName() + " and " + qt.getName() + "." ) ); diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentController.java index ea5c25c98c..35e0af1a7e 100644 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentController.java +++ b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentController.java @@ -1061,6 +1061,7 @@ private ModelAndView showAllSubSets( ExpressionExperiment ee, @Nullable Long dim .thenComparing( ( BioAssayDimension d ) -> quantitationTypesByDimension.get( d ).size(), Comparator.reverseOrder() ) .thenComparing( BioAssayDimension::getId ) ) ) .collect( Collectors.toMap( Map.Entry::getKey, e -> e.getValue().stream().sorted( Comparator.comparing( ExpressionExperimentSubSet::getName ) ).collect( Collectors.toList() ), ( a, b ) -> b, LinkedHashMap::new ) ); + QuantitationType singleCellQt = singleCellExpressionExperimentService.getPreferredSingleCellQuantitationType( ee ).orElse( null ); int offset = 0, limit = 20; Map heatmapsByDimension = new HashMap<>(); Map>> subSetFactorsByDimension = new HashMap<>(); @@ -1075,7 +1076,16 @@ private ModelAndView showAllSubSets( ExpressionExperiment ee, @Nullable Long dim List genes = designElements.stream() .map( de -> cs2gene.getOrDefault( de, Collections.emptyList() ).stream().findAny().orElse( null ) ) .collect( Collectors.toList() ); - heatmapsByDimension.put( dimension, ExpressionDataHeatmap.fromDesignElements( ee, dimension, designElements, genes ) ); + ExpressionDataHeatmap heatmap = ExpressionDataHeatmap.fromDesignElements( ee, dimension, designElements, genes ); + if ( singleCellQt != null ) { + heatmap.setSingleCellQuantitationType( singleCellQt ); + singleCellExpressionExperimentService.getPreferredCellTypeAssignment( ee, singleCellQt ) + .ifPresent( cta -> { + heatmap.setCellLevelCharacteristics( cta ); + heatmap.setFocusedCellLevelCharacteristic( null ); + } ); + } + heatmapsByDimension.put( dimension, heatmap ); } // reorganize the mapping to be easier to display expressionExperimentService.getSubSetsByFactorValue( ee, dimension ).entrySet() @@ -1146,6 +1156,19 @@ public ModelAndView showSubSet( @RequestParam("id") Long id, @RequestParam(value .map( de -> cs2gene.getOrDefault( de, Collections.emptyList() ).stream().findAny().orElse( null ) ) .collect( Collectors.toList() ); heatmap = ExpressionDataHeatmap.fromDesignElements( subset, dimension, designElements, genes ); + singleCellExpressionExperimentService.getPreferredSingleCellQuantitationType( subset.getSourceExperiment() ).ifPresent( scQt -> { + heatmap.setSingleCellQuantitationType( scQt ); + singleCellExpressionExperimentService.getPreferredCellTypeAssignment( subset.getSourceExperiment(), scQt ).ifPresent( cta -> { + heatmap.setCellLevelCharacteristics( cta ); + // TODO: use the subset factor if available to determine the cell type we want to focus on + cta.getCellTypes().stream() + .filter( c -> subset.getCharacteristics().stream() + .filter( c2 -> CharacteristicUtils.hasCategory( c2, Categories.CELL_TYPE ) ) + .anyMatch( c2 -> CharacteristicUtils.equals( c.getValue(), c.getValueUri(), c2.getValue(), c2.getValueUri() ) ) ) + .findAny() + .ifPresent( heatmap::setFocusedCellLevelCharacteristic ); + } ); + } ); heatmap.setTranspose( true ); } else { heatmap = null; diff --git a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentQCController.java b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentQCController.java index 51177c40d0..1f1e3551e4 100644 --- a/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentQCController.java +++ b/gemma-web/src/main/java/ubic/gemma/web/controller/expression/experiment/ExpressionExperimentQCController.java @@ -72,27 +72,31 @@ import ubic.gemma.core.analysis.preprocess.OutlierDetails; import ubic.gemma.core.analysis.preprocess.OutlierDetectionService; import ubic.gemma.core.analysis.preprocess.batcheffects.BatchInfoPopulationHelperServiceImpl; +import ubic.gemma.core.analysis.preprocess.convert.ScaleTypeConversionUtils; import ubic.gemma.core.analysis.preprocess.svd.SVDService; import ubic.gemma.core.analysis.preprocess.svd.SVDValueObject; import ubic.gemma.core.datastructure.matrix.io.ExperimentalDesignWriter; import ubic.gemma.core.util.BuildInfo; import ubic.gemma.core.visualization.ExpressionDataHeatmap; +import ubic.gemma.core.visualization.SingleCellDataBoxplot; import ubic.gemma.core.visualization.SingleCellSparsityHeatmap; import ubic.gemma.model.analysis.expression.coexpression.CoexpCorrelationDistribution; import ubic.gemma.model.analysis.expression.diff.DifferentialExpressionAnalysis; import ubic.gemma.model.analysis.expression.diff.ExpressionAnalysisResultSet; import ubic.gemma.model.common.quantitationtype.QuantitationType; +import ubic.gemma.model.common.quantitationtype.ScaleType; import ubic.gemma.model.expression.bioAssay.BioAssay; -import ubic.gemma.model.expression.bioAssayData.BioAssayDimension; -import ubic.gemma.model.expression.bioAssayData.MeanVarianceRelation; -import ubic.gemma.model.expression.bioAssayData.ProcessedExpressionDataVector; -import ubic.gemma.model.expression.bioAssayData.SingleCellDimension; +import ubic.gemma.model.expression.bioAssayData.*; +import ubic.gemma.model.expression.designElement.CompositeSequence; import ubic.gemma.model.expression.experiment.*; +import ubic.gemma.model.genome.Gene; import ubic.gemma.persistence.service.analysis.expression.coexpression.CoexpressionAnalysisService; import ubic.gemma.persistence.service.analysis.expression.diff.DifferentialExpressionAnalysisService; import ubic.gemma.persistence.service.analysis.expression.diff.ExpressionAnalysisResultSetService; import ubic.gemma.persistence.service.analysis.expression.sampleCoexpression.SampleCoexpressionAnalysisService; +import ubic.gemma.persistence.service.common.quantitationtype.QuantitationTypeService; import ubic.gemma.persistence.service.expression.bioAssayData.ProcessedExpressionDataVectorService; +import ubic.gemma.persistence.service.expression.designElement.CompositeSequenceService; import ubic.gemma.persistence.service.expression.experiment.ExpressionExperimentService; import ubic.gemma.persistence.service.expression.experiment.ExpressionExperimentSubSetService; import ubic.gemma.persistence.service.expression.experiment.SingleCellExpressionExperimentService; @@ -103,6 +107,7 @@ import ubic.gemma.web.util.WebEntityUrlBuilder; import ubic.gemma.web.view.TextView; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.awt.geom.Ellipse2D; @@ -113,7 +118,9 @@ import java.text.DecimalFormat; import java.util.List; import java.util.*; +import java.util.stream.Collectors; +import static java.util.Objects.requireNonNull; import static ubic.gemma.core.analysis.service.ExpressionDataFileUtils.*; import static ubic.gemma.core.datastructure.matrix.io.ExpressionDataWriterUtils.appendBaseHeader; @@ -167,9 +174,13 @@ public class ExpressionExperimentQCController extends BaseController { private ExpressionExperimentSubSetService expressionExperimentSubSetService; @Autowired private SingleCellExpressionExperimentService singleCellExpressionExperimentService; + @Autowired + private CompositeSequenceService compositeSequenceService; @Value("${gemma.analysis.dir}") private Path analysisStoragePath; + @Autowired + private QuantitationTypeService quantitationTypeService; @RequestMapping(value = "/expressionExperiment/detailedFactorAnalysis.html", method = { RequestMethod.GET, RequestMethod.HEAD }) public void detailedFactorAnalysis( @RequestParam("id") Long id, HttpServletResponse response ) throws Exception { @@ -590,6 +601,85 @@ public void visualizeSubSetHeatmap( @RequestParam("id") Long id, ChartUtils.writeBufferedImageAsPNG( response.getOutputStream(), image ); } + @RequestMapping(value = "/expressionExperiment/visualizeSingleCellDataBoxplot.html", method = { RequestMethod.GET, RequestMethod.HEAD }) + public void visualizeSingleCellDataBoxplot( @RequestParam("id") Long id, + @RequestParam(value = "quantitationType", required = false) + @Nullable Long quantitationTypeId, + @RequestParam("designElement") Long designElementId, + @RequestParam(value = "assays", required = false) Long[] assayIds, + @RequestParam(value = "cellTypeAssignment", required = false) String ctaName, + @RequestParam(value = "cellLevelCharacteristics", required = false) Long clcId, + @RequestParam(value = "focusedCharacteristic", required = false) Long focusedCharacteristicId, + HttpServletResponse response ) throws IOException { + if ( clcId != null && ctaName != null ) { + throw new IllegalArgumentException( "Cannot provide both 'cellTypeAssignment' and 'cellLevelCharacteristics' at the same time." ); + } + ExpressionExperiment ee = expressionExperimentService.loadOrFail( id, EntityNotFoundException::new ); + QuantitationType qt; + if ( quantitationTypeId != null ) { + qt = quantitationTypeService.loadByIdAndVectorType( quantitationTypeId, ee, SingleCellExpressionDataVector.class ); + if ( qt == null ) { + throw new EntityNotFoundException( ee + " does not have a quantitation type with ID " + quantitationTypeId + "." ); + } + } else { + qt = singleCellExpressionExperimentService.getPreferredSingleCellQuantitationType( ee ) + .orElseThrow( () -> new EntityNotFoundException( ee + " does not have a preferred single-cell quantitation type." ) ); + } + CompositeSequence designElement = compositeSequenceService.loadOrFail( designElementId, EntityNotFoundException::new ); + Collection genes; + genes = compositeSequenceService.getGenes( designElement ); + Gene gene; + if ( genes.isEmpty() ) { + log.warn( "No gene mapped by " + designElement ); + gene = null; + } else if ( genes.size() > 1 ) { + log.warn( "More than one gene mapped by " + designElement + "." ); + gene = null; + } else { + gene = genes.iterator().next(); + } + SingleCellExpressionDataVector vector = singleCellExpressionExperimentService.getSingleCellDataVectorWithoutCellIds( ee, qt, designElement ); + if ( vector == null ) { + throw new EntityNotFoundException( "No vector for design element with ID " + designElementId + "." ); + } + // TODO: cpm normalization + vector = ScaleTypeConversionUtils.convertVectors( Collections.singletonList( vector ), ScaleType.LOG10, SingleCellExpressionDataVector.class ).iterator().next(); + SingleCellDataBoxplot dataset = new SingleCellDataBoxplot( vector ); + dataset.setShowMean( false ); + if ( assayIds != null ) { + Map baMap = IdentifiableUtils.getIdMap( vector.getSingleCellDimension().getBioAssays() ); + List assays = Arrays.stream( assayIds ) + .map( baId -> requireNonNull( baMap.get( baId ), "BioAssay with ID " + baId + " does not belong to " + ee.getShortName() + "." ) ) + .collect( Collectors.toList() ); + dataset.setBioAssays( assays ); + } + if ( ctaName != null ) { + CellTypeAssignment cta = requireNonNull( singleCellExpressionExperimentService.getCellTypeAssignment( ee, qt, ctaName ) ); + dataset.setCellLevelCharacteristics( cta ); + if ( focusedCharacteristicId != null ) { + dataset.setFocusedCellLevelCharacteristic( cta.getCellTypes().stream().filter( cl -> cl.getId().equals( focusedCharacteristicId ) ).findFirst().orElseThrow( IllegalArgumentException::new ) ); + } + } + if ( clcId != null ) { + CellLevelCharacteristics clc = requireNonNull( singleCellExpressionExperimentService.getCellLevelCharacteristics( ee, qt, clcId ) ); + dataset.setCellLevelCharacteristics( clc ); + if ( focusedCharacteristicId != null ) { + dataset.setFocusedCellLevelCharacteristic( clc.getCharacteristics().stream().filter( cl -> cl.getId().equals( focusedCharacteristicId ) ).findFirst().orElseThrow( IllegalArgumentException::new ) ); + } + } + JFreeChart chart = ChartFactory.createBoxAndWhiskerChart( + "Single-cell expression data for " + ( gene != null ? gene.getOfficialSymbol() : designElement.getName() ) + " in " + ee.getShortName(), + "Assay", "Expression data (log10)", dataset.createDataset(), ctaName != null || clcId != null ); + if ( ( ctaName == null && clcId == null ) || focusedCharacteristicId != null ) { + // TODO: add per-row annotation, this does not appear to be supported by JFreeChart + dataset.createAnnotations().forEach( chart.getCategoryPlot()::addAnnotation ); + } + response.setContentType( MediaType.IMAGE_PNG_VALUE ); + ChartUtils.writeChartAsPNG( response.getOutputStream(), chart, + Math.min( Math.max( 50 * dataset.getNumberOfBoxplots(), DEFAULT_QC_IMAGE_SIZE_PX ), MAX_QC_IMAGE_SIZE_PX ), + DEFAULT_QC_IMAGE_SIZE_PX ); + } + private void addChartToGraphics( JFreeChart chart, Graphics2D g2, double x, double y, double width, double height ) { chart.draw( g2, new Rectangle2D.Double( x, y, width, height ), null, null ); diff --git a/gemma-web/src/main/java/ubic/gemma/web/taglib/expression/experiment/ExpressionDataHeatmapTag.java b/gemma-web/src/main/java/ubic/gemma/web/taglib/expression/experiment/ExpressionDataHeatmapTag.java index 4686682434..630155f55a 100644 --- a/gemma-web/src/main/java/ubic/gemma/web/taglib/expression/experiment/ExpressionDataHeatmapTag.java +++ b/gemma-web/src/main/java/ubic/gemma/web/taglib/expression/experiment/ExpressionDataHeatmapTag.java @@ -112,6 +112,32 @@ public void writeGenes( TagWriter writer ) throws JspException { writer.writeAttribute( "href", entityUrlBuilder.fromContextPath().entity( gene ).toUriString() ); writer.appendValue( gene.getOfficialSymbol() ); writer.endTag(); // + if ( heatmap.getSingleCellQuantitationType() != null ) { + CompositeSequence designElement; + if ( heatmap.getVectors() != null ) { + designElement = heatmap.getVectors().get( i ).getDesignElement(); + } else if ( heatmap.getDesignElements() != null ) { + designElement = heatmap.getDesignElements().get( i ); + } else { + throw new IllegalStateException( "An expression data heatmap must have either vectors or design elements populated." ); + } + ExpressionExperiment ee; + if ( heatmap.getBioAssaySet() instanceof ExpressionExperimentSubSet ) { + ee = ( ( ExpressionExperimentSubSet ) heatmap.getBioAssaySet() ).getSourceExperiment(); + } else if ( heatmap.getBioAssaySet() instanceof ExpressionExperiment ) { + ee = ( ExpressionExperiment ) heatmap.getBioAssaySet(); + } else { + throw new IllegalStateException( "Unexpected BioAssaySet type in heatmap: " + heatmap.getBioAssaySet().getClass().getName() ); + } + String boxplotUrl = entityUrlBuilder.fromContextPath().entity( ee ) + .web() + .visualizeSingleCellBoxPlot( heatmap.getSingleCellQuantitationType(), designElement, heatmap.getCellLevelCharacteristics(), heatmap.getFocusedCellLevelCharacteristic() ) + .toUriString(); + writer.startTag( "a" ); + writer.writeAttribute( "href", boxplotUrl ); + writer.appendValue( " (view single-cell expression data)" ); + writer.endTag(); // + } } else { writer.startTag( "i" ); writer.appendValue( "Unmapped: " );