|
| 1 | +package is.hail.types.arrow |
| 2 | + |
| 3 | +import is.hail.types.physical._ |
| 4 | +import is.hail.utils.fatal |
| 5 | +import is.hail.variant.ReferenceGenome |
| 6 | + |
| 7 | +import scala.jdk.CollectionConverters._ |
| 8 | + |
| 9 | +import org.apache.arrow.vector.types.FloatingPointPrecision |
| 10 | +import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType, Schema} |
| 11 | + |
| 12 | +/** Conversion between Hail physical types and the Arrow schema of the columnar format. |
| 13 | + * |
| 14 | + * Arrow nullability carries Hail requiredness at every level, with one exception: dict keys are |
| 15 | + * always non-nullable, so a dict whose key type is optional reads back with a required key type. |
| 16 | + * |
| 17 | + * Arrow -> Hail accepts exactly the Arrow types that Hail -> Arrow produces. |
| 18 | + */ |
| 19 | +object ArrowSchemaConversion { |
| 20 | + ExtensionTypes.register() |
| 21 | + |
| 22 | + val ElementFieldName: String = "element" |
| 23 | + val EntriesFieldName: String = "entries" |
| 24 | + val KeyFieldName: String = "key" |
| 25 | + val ValueFieldName: String = "value" |
| 26 | + |
| 27 | + private val Int32 = new ArrowType.Int(32, true) |
| 28 | + private val Int64 = new ArrowType.Int(64, true) |
| 29 | + private val Float32 = new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE) |
| 30 | + private val Float64 = new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) |
| 31 | + |
| 32 | + private val IntervalFieldNames = |
| 33 | + IndexedSeq("start", "end", "includes_start", "includes_end") |
| 34 | + |
| 35 | + private val TensorFieldNames = IndexedSeq("data", "shape") |
| 36 | + |
| 37 | + def toArrowSchema(t: PStruct, referenceGenomes: Map[String, ReferenceGenome]): Schema = |
| 38 | + new Schema(t.fields.map(f => toArrowField(f.name, f.typ, referenceGenomes)).asJava) |
| 39 | + |
| 40 | + def toArrowField(name: String, t: PType, referenceGenomes: Map[String, ReferenceGenome]) |
| 41 | + : Field = { |
| 42 | + def field(arrowType: ArrowType, children: Field*): Field = |
| 43 | + mkField(name, !t.required, arrowType, children: _*) |
| 44 | + |
| 45 | + def child(name: String, t: PType): Field = |
| 46 | + toArrowField(name, t, referenceGenomes) |
| 47 | + |
| 48 | + t match { |
| 49 | + case _: PBoolean => field(ArrowType.Bool.INSTANCE) |
| 50 | + case _: PInt32 => field(Int32) |
| 51 | + case _: PInt64 => field(Int64) |
| 52 | + case _: PFloat32 => field(Float32) |
| 53 | + case _: PFloat64 => field(Float64) |
| 54 | + case _: PString => field(ArrowType.Utf8.INSTANCE) |
| 55 | + case _: PBinary => field(ArrowType.Binary.INSTANCE) |
| 56 | + case _: PCall => field(CallExtensionType.Current) |
| 57 | + case t: PLocus => |
| 58 | + val rg = referenceGenomes.getOrElse( |
| 59 | + t.rg, |
| 60 | + fatal(s"field '$name': reference genome '${t.rg}' is not defined"), |
| 61 | + ) |
| 62 | + field(new LocusExtensionType(rg.name, rg.contigs)) |
| 63 | + case t: PInterval => |
| 64 | + field( |
| 65 | + IntervalExtensionType, |
| 66 | + child(IntervalFieldNames(0), t.pointType), |
| 67 | + child(IntervalFieldNames(1), t.pointType), |
| 68 | + mkField(IntervalFieldNames(2), nullable = false, ArrowType.Bool.INSTANCE), |
| 69 | + mkField(IntervalFieldNames(3), nullable = false, ArrowType.Bool.INSTANCE), |
| 70 | + ) |
| 71 | + case t: PNDArray => |
| 72 | + field( |
| 73 | + VariableShapeTensorExtensionType, |
| 74 | + mkField( |
| 75 | + TensorFieldNames(0), |
| 76 | + nullable = false, |
| 77 | + ArrowType.List.INSTANCE, |
| 78 | + child(ElementFieldName, t.elementType), |
| 79 | + ), |
| 80 | + mkField( |
| 81 | + TensorFieldNames(1), |
| 82 | + nullable = false, |
| 83 | + new ArrowType.FixedSizeList(t.nDims), |
| 84 | + mkField(ElementFieldName, nullable = false, Int32), |
| 85 | + ), |
| 86 | + ) |
| 87 | + case t: PSet => field(SetExtensionType, child(ElementFieldName, t.elementType)) |
| 88 | + case t: PDict => |
| 89 | + field( |
| 90 | + new ArrowType.Map( /*keysSorted=*/ true), |
| 91 | + mkField( |
| 92 | + EntriesFieldName, |
| 93 | + nullable = false, |
| 94 | + ArrowType.Struct.INSTANCE, |
| 95 | + child(KeyFieldName, t.keyType.setRequired(true)), |
| 96 | + child(ValueFieldName, t.valueType), |
| 97 | + ), |
| 98 | + ) |
| 99 | + case t: PArray => field(ArrowType.List.INSTANCE, child(ElementFieldName, t.elementType)) |
| 100 | + case t: PTuple => field(TupleExtensionType, t.fields.map(f => child(f.name, f.typ)): _*) |
| 101 | + case t: PStruct => |
| 102 | + field(ArrowType.Struct.INSTANCE, t.fields.map(f => child(f.name, f.typ)): _*) |
| 103 | + case _ => |
| 104 | + fatal(s"field '$name': Hail type ${t.virtualType} cannot be stored in the columnar format") |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + def fromArrowSchema(schema: Schema, referenceGenomes: Map[String, ReferenceGenome]) |
| 109 | + : PCanonicalStruct = |
| 110 | + PCanonicalStruct( |
| 111 | + structFields(schema.getFields.asScala.toIndexedSeq, referenceGenomes), |
| 112 | + required = true, |
| 113 | + ) |
| 114 | + |
| 115 | + def fromArrowField(field: Field, referenceGenomes: Map[String, ReferenceGenome]): PType = { |
| 116 | + val name = field.getName |
| 117 | + val required = !field.isNullable |
| 118 | + val children = field.getChildren.asScala.toIndexedSeq |
| 119 | + |
| 120 | + def unsupported(reason: String): Nothing = |
| 121 | + fatal( |
| 122 | + s"field '$name' of Arrow type ${field.getType} is not part of the columnar format: $reason" |
| 123 | + ) |
| 124 | + |
| 125 | + def checkChildren(expected: IndexedSeq[String]): Unit = |
| 126 | + if (children.map(_.getName) != expected) |
| 127 | + unsupported( |
| 128 | + s"expected child fields ${expected.mkString("[", ", ", "]")}, found ${children.map(_.getName).mkString("[", ", ", "]")}" |
| 129 | + ) |
| 130 | + |
| 131 | + def onlyChild(): Field = |
| 132 | + if (children.length == 1) children.head |
| 133 | + else unsupported(s"expected 1 child field, found ${children.length}") |
| 134 | + |
| 135 | + def convert(f: Field): PType = fromArrowField(f, referenceGenomes) |
| 136 | + |
| 137 | + def requiredChild(f: Field): Field = |
| 138 | + if (f.isNullable) unsupported(s"child field '${f.getName}' must be non-nullable") |
| 139 | + else f |
| 140 | + |
| 141 | + if (field.getDictionary != null) |
| 142 | + unsupported("dictionary encoding is not supported") |
| 143 | + |
| 144 | + field.getType match { |
| 145 | + case _: CallExtensionType if field.getType == CallExtensionType.Current => |
| 146 | + PCanonicalCall(required) |
| 147 | + case t: CallExtensionType => |
| 148 | + unsupported(s"unknown call encoding ${t.encoding}") |
| 149 | + case t: LocusExtensionType => |
| 150 | + val rg = referenceGenomes.getOrElse( |
| 151 | + t.referenceGenome, |
| 152 | + fatal(s"field '$name': reference genome '${t.referenceGenome}' is not defined"), |
| 153 | + ) |
| 154 | + if (rg.contigs != t.contigs) |
| 155 | + fatal( |
| 156 | + s"field '$name': the stored contigs of reference genome '${t.referenceGenome}' " + |
| 157 | + "differ from the contigs of the defined reference genome" |
| 158 | + ) |
| 159 | + PCanonicalLocus(rg.name, required) |
| 160 | + case SetExtensionType => |
| 161 | + PCanonicalSet(convert(onlyChild()), required) |
| 162 | + case IntervalExtensionType => |
| 163 | + checkChildren(IntervalFieldNames) |
| 164 | + val IndexedSeq(start, end, includesStart, includesEnd) = children |
| 165 | + val pointType = convert(start) |
| 166 | + if (convert(end) != pointType) |
| 167 | + unsupported("interval 'start' and 'end' have different types") |
| 168 | + for (f <- Seq(includesStart, includesEnd)) |
| 169 | + if (f.isNullable || f.getType != ArrowType.Bool.INSTANCE || !f.getChildren.isEmpty) |
| 170 | + unsupported(s"interval '${f.getName}' must be a non-nullable bool") |
| 171 | + PCanonicalInterval(pointType, required) |
| 172 | + case TupleExtensionType => |
| 173 | + val fields = children.map { c => |
| 174 | + val idx = c.getName.toIntOption.filter(i => i >= 0 && i.toString == c.getName).getOrElse( |
| 175 | + unsupported(s"tuple field name '${c.getName}' is not a non-negative integer") |
| 176 | + ) |
| 177 | + PTupleField(idx, convert(c)) |
| 178 | + } |
| 179 | + if (fields.map(_.index).distinct.length != fields.length) |
| 180 | + unsupported("tuple field names are not distinct") |
| 181 | + PCanonicalTuple(fields, required) |
| 182 | + case VariableShapeTensorExtensionType => |
| 183 | + checkChildren(TensorFieldNames) |
| 184 | + val IndexedSeq(data, shape) = children |
| 185 | + val nDims = shape.getType match { |
| 186 | + case t: ArrowType.FixedSizeList => t.getListSize |
| 187 | + case t => unsupported(s"tensor 'shape' must be a fixed_size_list, found $t") |
| 188 | + } |
| 189 | + requiredChild(shape) |
| 190 | + if (shape.getChildren.size != 1 || convert(shape.getChildren.get(0)) != PInt32(true)) |
| 191 | + unsupported("tensor 'shape' must contain non-nullable int32") |
| 192 | + if (requiredChild(data).getType != ArrowType.List.INSTANCE || data.getChildren.size != 1) |
| 193 | + unsupported("tensor 'data' must be a list") |
| 194 | + val elementType = convert(requiredChild(data.getChildren.get(0))) |
| 195 | + if (elementType.containsPointers) |
| 196 | + unsupported(s"tensor element type ${elementType.virtualType} is not supported") |
| 197 | + PCanonicalNDArray(elementType, nDims, required) |
| 198 | + case t: ArrowType.ExtensionType => |
| 199 | + unsupported(s"unknown extension type '${t.extensionName}'") |
| 200 | + case _ |
| 201 | + if field.getMetadata.containsKey(ArrowType.ExtensionType.EXTENSION_METADATA_KEY_NAME) => |
| 202 | + unsupported( |
| 203 | + s"unregistered extension type '${field.getMetadata.get(ArrowType.ExtensionType.EXTENSION_METADATA_KEY_NAME)}'" |
| 204 | + ) |
| 205 | + case ArrowType.Bool.INSTANCE => PBoolean(required) |
| 206 | + case Int32 => PInt32(required) |
| 207 | + case Int64 => PInt64(required) |
| 208 | + case Float32 => PFloat32(required) |
| 209 | + case Float64 => PFloat64(required) |
| 210 | + case ArrowType.Utf8.INSTANCE => PCanonicalString(required) |
| 211 | + case ArrowType.Binary.INSTANCE => PCanonicalBinary(required) |
| 212 | + case ArrowType.List.INSTANCE => PCanonicalArray(convert(onlyChild()), required) |
| 213 | + case t: ArrowType.Map => |
| 214 | + if (!t.getKeysSorted) unsupported("map keys must be sorted") |
| 215 | + val entries = requiredChild(onlyChild()) |
| 216 | + if (entries.getType != ArrowType.Struct.INSTANCE || entries.getChildren.size != 2) |
| 217 | + unsupported("map entries must be a struct with two fields") |
| 218 | + val key = requiredChild(entries.getChildren.get(0)) |
| 219 | + PCanonicalDict(convert(key), convert(entries.getChildren.get(1)), required) |
| 220 | + case ArrowType.Struct.INSTANCE => |
| 221 | + PCanonicalStruct(structFields(children, referenceGenomes), required) |
| 222 | + case _ => unsupported("unsupported type") |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + private def structFields( |
| 227 | + fields: IndexedSeq[Field], |
| 228 | + referenceGenomes: Map[String, ReferenceGenome], |
| 229 | + ): IndexedSeq[PField] = { |
| 230 | + val duplicates = fields.map(_.getName).groupBy(identity).collect { |
| 231 | + case (name, names) if names.length > 1 => name |
| 232 | + } |
| 233 | + if (duplicates.nonEmpty) |
| 234 | + fatal( |
| 235 | + s"duplicate field names are not part of the columnar format: ${duplicates.mkString(", ")}" |
| 236 | + ) |
| 237 | + fields.zipWithIndex.map { case (f, i) => |
| 238 | + PField(f.getName, fromArrowField(f, referenceGenomes), i) |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + private def mkField(name: String, nullable: Boolean, arrowType: ArrowType, children: Field*) |
| 243 | + : Field = |
| 244 | + new Field(name, new FieldType(nullable, arrowType, /*dictionary=*/ null), children.asJava) |
| 245 | +} |
0 commit comments