org.scijava.ops.engine.matcher.convert.ConvertedOpInfo Maven / Gradle / Ivy
Show all versions of scijava-ops-engine Show documentation
/*-
* #%L
* Java implementation of the SciJava Ops matching engine.
* %%
* Copyright (C) 2016 - 2024 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package org.scijava.ops.engine.matcher.convert;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.*;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import javassist.*;
import org.scijava.common3.Comparisons;
import org.scijava.function.Computers;
import org.scijava.meta.Versions;
import org.scijava.ops.api.*;
import org.scijava.ops.engine.BaseOpHints;
import org.scijava.ops.engine.conversionLoss.LossReporter;
import org.scijava.ops.engine.struct.FunctionalMethodType;
import org.scijava.ops.engine.struct.OpRetypingMemberParser;
import org.scijava.ops.engine.struct.RetypingRequest;
import org.scijava.ops.engine.util.Infos;
import org.scijava.priority.Priority;
import org.scijava.struct.ItemIO;
import org.scijava.struct.Member;
import org.scijava.struct.Struct;
import org.scijava.struct.StructInstance;
import org.scijava.struct.Structs;
import org.scijava.common3.Any;
import org.scijava.types.Nil;
import org.scijava.common3.Types;
import org.scijava.types.infer.FunctionalInterfaces;
import org.scijava.types.infer.GenericAssignability;
/**
* An {@link OpInfo} whose input and output types are transformed through the
* use of {@code engine.convert} {@link Function} Ops. Op instances created by
* this {@code OpInfo} utilize a {@code engine.convert} "preconverter" Op to
* coerce each provided input to the type expected by the Op, and after
* execution use a {@code engine.convert} "postconverter" Op to coerce the
* output back into the type expected by the user. If the Op defines a
* preallocated output buffer, a {@code engine.copy} {@link Computers.Arity1} Op
* is used to place the computational results of the Op back into the passed
* output buffer.
*
* As an example, consider a {@code Function}. If we have a
* {@code engine.convert} Op that goes from {@link Integer} to {@link Double}
* and a {@code engine.convert} Op that goes from {@link Double} to
* {@link Integer} then we can construct a {@code Function}
* Op. At runtime, we will utilize the former {@code engine.convert} Op to
* preconvert the input {@code Integer} into a {@code Double}, invoke the
* {@code Function}, and then postconvert the {@code Double}
* into an {@code Integer} using the latter {@code engine.convert} Op, which is
* returned to the user.
*
*
* @author Gabriel Selzer
*/
public class ConvertedOpInfo implements OpInfo {
/** Identifiers for declaring a conversion in an Op signature **/
protected static final String IMPL_DECLARATION = "|Conversion:";
protected static final String PRECONVERTER_DELIMITER = "|Preconverter:";
protected static final String POSTCONVERTER_DELIMITER = "|Postconverter:";
protected static final String OUTPUT_COPIER_DELIMITER = "|OutputCopier:";
protected static final String ORIGINAL_INFO = "|OriginalInfo:";
private final OpInfo info;
private final OpEnvironment env;
private final Map, Type> typeVarAssigns;
final List>> preconverters;
final List inTypes;
final RichOp> postconverter;
final Type outType;
final RichOp> copyOp;
private final Type opType;
private final Struct struct;
private Double priority = null;
private final Hints hints;
public ConvertedOpInfo(OpInfo info,
List>> preconverters,
RichOp> postconverter,
final RichOp> copyOp, OpEnvironment env)
{
this( //
info, //
generateOpType(info, preconverters, postconverter), //
preconverters, //
Arrays.asList(inTypes(info.inputTypes(), preconverters)), //
postconverter, //
outType(info.outputType(), postconverter), copyOp, //
env, //
Collections.emptyMap() //
);
}
private static Type generateOpType(OpInfo info,
List>> preconverter,
RichOp> postconverter)
{
Type[] inTypes = inTypes(info.inputTypes(), preconverter);
Type outType = outType(info.outputType(), postconverter);
Class> fIface = FunctionalInterfaces.findFrom(info.opType());
return retypeOpType(fIface, inTypes, outType);
}
public ConvertedOpInfo( //
OpInfo info, //
Type opType, //
List>> preconverters, //
List reqInputs, //
RichOp> postconverter, //
Type reqOutput, //
final RichOp> copyOp, //
OpEnvironment env, //
Map, Type> typeVarAssigns //
) {
this.info = info;
this.opType = mapAnys(opType, info);
this.preconverters = preconverters;
this.inTypes = reqInputs;
this.postconverter = postconverter;
this.outType = reqOutput;
this.copyOp = copyOp;
this.env = env;
this.struct = generateStruct();
this.hints = info.declaredHints().plus( //
BaseOpHints.Conversion.FORBIDDEN, //
"converted" //
);
this.typeVarAssigns = typeVarAssigns;
}
/**
* Helper method to generate the new {@link Struct}
*
* @return the {@link Struct} of the converted Op
*/
private Struct generateStruct() {
List originalIns = new ArrayList<>(info.inputTypes());
List> ioMembers = new ArrayList<>(info.struct().members());
// If the mutable index differs between the declared Op type and the
// requested Op type, we must move the IO memberr
int fromIOIdx = Conversions.mutableIndexOf(Types.raw(info.opType()));
int toIOIdx = Conversions.mutableIndexOf(Types.raw(opType));
if (fromIOIdx != toIOIdx) {
originalIns.add(toIOIdx, originalIns.remove(fromIOIdx));
ioMembers.add(toIOIdx, ioMembers.remove(fromIOIdx));
}
// Create the functional member types of the new OpInfo
int index = 0;
List fmts = new ArrayList<>();
for (Member> m : ioMembers) {
if (m.getIOType() == ItemIO.NONE) continue;
Type newType = m.isInput() ? this.inTypes.get(index++) : m.isOutput()
? outType : null;
fmts.add(new FunctionalMethodType(newType, m.getIOType()));
}
// generate new struct
RetypingRequest r = new RetypingRequest(info.struct(), fmts);
return Structs.from(r, opType, new OpRetypingMemberParser());
}
public OpInfo srcInfo() {
return info;
}
@Override
public List names() {
return srcInfo().names();
}
@Override
public String description() {
return srcInfo().description();
}
@Override
public Type opType() {
return opType;
}
@Override
public Hints declaredHints() {
return hints;
}
@Override
public Struct struct() {
return struct;
}
@Override
public String implementationName() {
StringBuilder sb = new StringBuilder(info.implementationName());
sb.append("|converted");
for (Member> m : struct()) {
if (m.isInput() || m.isOutput()) {
sb.append("_");
sb.append(Conversions.getClassName(m.type()));
}
}
return sb.toString();
}
@Override
public AnnotatedElement getAnnotationBearer() {
return info.getAnnotationBearer();
}
private final Function, ?> IGNORED = t -> t;
/**
* Creates a converted version of the original Op, whose parameter
* types are dictated by the input types of this info's preconverters. The
* resulting Op uses those preconverters to convert the inputs. After invoking
* the original Op, this Op will use this info's postconverters to convert the
* output into the type requested by the user. If the request defines a
* preallocated output buffer, this Op will also take care to copy the
* postconverted output back into the user-provided output buffer.
*
* @param dependencies - this Op's dependencies
*/
@Override
public StructInstance> createOpInstance(List> dependencies) {
final Object op = info.createOpInstance(dependencies).object();
try {
Object convertedOp = javassistOp( //
op, //
this, //
this.preconverters.stream().map(rich -> {
if (rich == null) {
return IGNORED;
}
return rich.asOpType();
}).collect(Collectors.toList()), //
this.postconverter == null ? IGNORED : this.postconverter.asOpType(), //
this.copyOp == null ? null : this.copyOp.asOpType() //
);
return struct().createInstance(convertedOp);
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Failed to invoke parameter conversion of Op: \n" + info +
"\nProvided Op dependencies were: " + dependencies, ex);
}
}
@Override
public String toString() {
return Infos.describe(this);
}
@Override
public int compareTo(final OpInfo that) {
// compare priorities
if (this.priority() < that.priority()) return 1;
if (this.priority() > that.priority()) return -1;
// compare implementation names
int implNameDiff = Comparisons.compare(this.implementationName(), that
.implementationName());
if (implNameDiff != 0) return implNameDiff;
// compare structs if the OpInfos are "sibling" ConvertedOpInfos
if (that instanceof ConvertedOpInfo) return compareConvertedInfos(
(ConvertedOpInfo) that);
return 0;
}
private int compareConvertedInfos(ConvertedOpInfo that) {
// Compare structs
int thisHash = this.struct().members().hashCode();
int thatHash = that.struct().members().hashCode();
return thisHash - thatHash;
}
@Override
public String version() {
return Versions.of(this.getClass());
}
/**
* For a converted Op, we define the implementation as the concatenation of:
*
* - The signature of all preconverters
* - The signature of the postconverter
* - The signature of the output copier
* - The id of the source Op
*
*
*/
@Override
public String id() {
// original Op
StringBuilder sb = new StringBuilder(IMPL_DECLARATION);
// preconverters
for (RichOp> i : preconverters) {
sb.append(PRECONVERTER_DELIMITER);
sb.append(i.infoTree().signature());
}
// postconverter
sb.append(POSTCONVERTER_DELIMITER);
sb.append(postconverter.infoTree().signature());
// output copier
if (copyOp != null) {
sb.append(OUTPUT_COPIER_DELIMITER);
sb.append(copyOp.infoTree().signature());
}
// original info
sb.append(ORIGINAL_INFO);
sb.append(srcInfo().id());
return sb.toString();
}
private static Type[] inTypes(List originalInputs,
List>> preconverters)
{
Map, Type> typeAssigns = new HashMap<>();
Type[] inTypes = new Type[originalInputs.size()];
for (int i = 0; i < originalInputs.size(); i++) {
typeAssigns.clear();
// Start by looking at the input type T of the preconverter
var type = preconverters.get(i).instance().type();
var pType = (ParameterizedType) type;
inTypes[i] = pType.getActualTypeArguments()[0];
// Sometimes, the type of the Op instance can contain wildcards.
// These do not help us in determining the new type, so we have to
// start over with the input converter input type.
if (inTypes[i] instanceof WildcardType) {
inTypes[i] = Ops.info(preconverters.get(i)).inputTypes().get(0);
}
// Infer type variables in the preconverter input w.r.t. the
// parameter types of the ORIGINAL OpInfo
GenericAssignability.inferTypeVariables( //
new Type[] { pType.getActualTypeArguments()[1] }, //
new Type[] { originalInputs.get(i) }, //
typeAssigns //
);
// Map type variables in T to Types inferred above
inTypes[i] = Types.unroll(inTypes[i], typeAssigns);
}
return inTypes;
}
private static Type outType( //
Type originalOutput, //
RichOp> postconverter //
) {
if (postconverter == null) {
return originalOutput;
}
// Start by looking at the output type T of the postconverter
var type = postconverter.instance().type();
var pType = (ParameterizedType) type;
Type outType = pType.getActualTypeArguments()[1];
// Sometimes, the type of the Op instance can contain wildcards.
// These do not help us in determining the new type, so we have to
// start over with the postconverter's output type.
if (outType instanceof WildcardType) {
outType = Ops.info(postconverter).outputType();
}
Map, Type> vars = new HashMap<>();
// Infer type variables in the postconverter input w.r.t. the
// parameter types of the ORIGINAL OpInfo
GenericAssignability.inferTypeVariables( //
new Type[] { pType.getActualTypeArguments()[0] }, //
new Type[] { originalOutput }, //
vars //
);
// map type variables in T to Types inferred in Step 2a
return Types.unroll(outType, vars);
}
@Override
public double priority() {
if (priority == null) {
priority = calculatePriority(env);
}
return priority;
}
/**
* Helper method that removes any {@link Any}s in the Op type, replacing them
* with the types declared by the original {@link OpInfo}
*
* @param opType the Op {@link Type}, which may have some {@link Any}s
* @param info the original {@link OpInfo} being wrapped
* @return {@code opType}, without any {@link Any}s.
*/
private static Type mapAnys(Type opType, OpInfo info) {
var raw = Types.parameterize(Types.raw(opType));
Map, Type> reqMap = new HashMap<>();
GenericAssignability.inferTypeVariables(new Type[] { raw }, new Type[] {
opType }, reqMap);
Map, Type> infoMap = new HashMap<>();
GenericAssignability.inferTypeVariables(new Type[] { raw }, new Type[] {
info.opType() }, infoMap);
for (var key : reqMap.keySet()) {
var val = reqMap.get(key);
if (Any.is(val)) {
reqMap.put(key, infoMap.get(key));
}
}
return Types.unroll(raw, reqMap);
}
/**
* We define the priority of any {@link ConvertedOpInfo} as the sum of the
* following:
*
* - {@link Priority#VERY_LOW} to ensure that conversions are not chosen
* over a direct match.
* - The {@link OpInfo#priority} of the source info to ensure that the
* conversion of a higher-priority Op wins out over the conversion of a
* lower-priority Op, all else equal.
* - a penalty defined as a lossiness heuristic of this conversion. This
* penalty is the sum of:
*
* - the loss undertaken by converting each of the Op's inputs from the ref
* type to the info type
* - the loss undertaken by converting each of the Op's outputs from the
* info type to the ref type
*
*
*/
private double calculatePriority(OpEnvironment env) {
// BASE PRIORITY
double base = Priority.VERY_LOW;
// ORIGINAL PRIORITY
double originalPriority = info.priority();
// PENALTY
double penalty = 0;
List originalInputs = info.inputTypes();
List inputs = inputTypes();
for (int i = 0; i < inputs.size(); i++) {
var from = inputs.get(i);
var to = Types.unroll(originalInputs.get(i), typeVarAssigns);
penalty += determineLoss(env, Nil.of(from), Nil.of(to));
}
Type opOutput = info.outputType();
penalty += determineLoss(env, Nil.of(opOutput), Nil.of(outputType()));
// PRIORITY = BASE + ORIGINAL - PENALTY
return base + originalPriority - penalty;
}
/**
* Calls a {@code engine.lossReporter} Op to determine the worst-case
* loss from a {@code T} to a {@code R}. If no {@code engine.lossReporter}
* exists for such a conversion, we assume infinite loss.
*
* @param -the generic type we are converting from.
* @param - generic type we are converting to.
* @param from - a {@link Nil} describing the type we are converting from
* @param to - a {@link Nil} describing the type we are converting to
* @return - a {@code double} describing the magnitude of the
* loss in a conversion from an instance of {@code T} to an instance
* of {@code R}
*/
private static double determineLoss(OpEnvironment env, Nil from,
Nil to)
{
Type specialType = Types.parameterize(LossReporter.class, new Type[] { from
.type(), to.type() });
@SuppressWarnings("unchecked")
Nil> specialTypeNil = (Nil>) Nil.of(
specialType);
try {
Type nilFromType = Types.parameterize(Nil.class, new Type[] { from
.type() });
Type nilToType = Types.parameterize(Nil.class, new Type[] { to
.type() });
Hints h = new Hints( //
BaseOpHints.Adaptation.FORBIDDEN, //
BaseOpHints.Conversion.FORBIDDEN, //
BaseOpHints.History.IGNORE //
);
LossReporter op = env.op("engine.lossReporter", specialTypeNil,
new Nil[] { Nil.of(nilFromType), Nil.of(nilToType) }, Nil.of(
Double.class), h);
return op.apply(from, to);
}
catch (OpMatchingException e) {
return Double.POSITIVE_INFINITY;
}
}
/**
* Determines the {@link Type} of a retyped Op using its old {@code Type}, a
* new set of {@code args} and a new {@code outType}. Used to create
* {@link ConvertedOpInfo}s. This method assumes that:
*
* - {@code originalOpRefType} is (or is a subtype of) some
* {@link FunctionalInterface}
* - all {@link TypeVariable}s declared by that {@code FunctionalInterface}
* are present in the signature of that interface's single abstract
* method.
*
*
* @param originalOpType - the {@link Type} declared by the source
* {@link OpRequest}
* @param newArgs - the new argument {@link Type}s requested by the
* {@link OpRequest}.
* @param newOutType - the new output {@link Type} requested by the
* {@link OpRequest}.
* @return - a new {@code type} for a {@link ConvertedOpInfo}.
*/
private static ParameterizedType retypeOpType(Type originalOpType,
Type[] newArgs, Type newOutType)
{
// only retype types that we know how to retype
Class> opType = Types.raw(originalOpType);
Method fMethod = FunctionalInterfaces.functionalMethodOf(opType);
Map, Type> typeVarAssigns = new HashMap<>();
// solve input types
Type[] genericParameterTypes = paramTypesFromOpType(opType, fMethod);
GenericAssignability.inferTypeVariables(genericParameterTypes, newArgs,
typeVarAssigns);
// solve output type
Type genericReturnType = returnTypeFromOpType(opType, fMethod);
if (genericReturnType != void.class) {
GenericAssignability.inferTypeVariables(new Type[] { genericReturnType },
new Type[] { newOutType }, typeVarAssigns);
}
// build new (read: converted) Op type
return Types.parameterize(opType, typeVarAssigns);
}
private static Type[] paramTypesFromOpType(Class> opType, Method fMethod) {
Type[] genericParameterTypes = fMethod.getGenericParameterTypes();
if (fMethod.getDeclaringClass().equals(opType))
return genericParameterTypes;
return typesFromOpType(opType, fMethod, genericParameterTypes);
}
private static Type returnTypeFromOpType(Class> opType, Method fMethod) {
Type genericReturnType = fMethod.getGenericReturnType();
if (fMethod.getDeclaringClass().equals(opType)) return genericReturnType;
return typesFromOpType(opType, fMethod, genericReturnType)[0];
}
private static Type[] typesFromOpType(Class> opType, Method fMethod,
Type... types)
{
Map, Type> map = new HashMap<>();
Class> declaringClass = fMethod.getDeclaringClass();
Type genericDeclaringClass = Types.parameterize(declaringClass);
Type genericClass = Types.parameterize(opType);
Type superGenericClass = Types.superTypeOf(genericClass,
declaringClass);
GenericAssignability.inferTypeVariables(new Type[] {
genericDeclaringClass }, new Type[] { superGenericClass }, map);
return Types.unroll(types, map);
}
/**
* Creates a Converted Op class. This class:
*
* - is of the same functional type as the given Op
* - has type arguments that are of the converted form of the type arguments
* of the given Op (these arguments are dictated by the
* {@code preconverters}s.
* -
*
* @param originalOp - the Op that will be converted
* @return a wrapper of {@code originalOp} taking arguments that are then
* mutated to satisfy {@code originalOp}, producing outputs that are
* then mutated to satisfy the desired output of the wrapper.
* @throws Throwable in the case of an error
*/
private static Object javassistOp( //
Object originalOp, //
OpInfo alteredInfo, //
List
> preconverters, //
Function, ?> postconverter, //
Computers.Arity1, ?> copyOp //
) throws Throwable {
ClassPool pool = ClassPool.getDefault();
// Create wrapper class
String className = formClassName(alteredInfo);
Class> c;
try {
c = pool.getClassLoader().loadClass(className);
}
catch (ClassNotFoundException e) {
CtClass cc = generateConvertedClass( //
pool, //
className, //
alteredInfo, //
preconverters, //
//
copyOp //
);
c = cc.toClass(MethodHandles.lookup());
}
// Return Op instance
return c.getDeclaredConstructor(constructorClasses(alteredInfo,
copyOp != null)).newInstance(constructorArgs(preconverters, postconverter,
copyOp, originalOp));
}
private static Class>[] constructorClasses( //
OpInfo originalInfo, //
boolean addCopyOp //
) {
// there are 2*numInputs input mutators, 2 output mutators
int numMutators = originalInfo.inputTypes().size() + 1;
// original Op plus a output copier if applicable
int numOps = addCopyOp ? 2 : 1;
Class>[] args = new Class>[numMutators + numOps];
for (int i = 0; i < numMutators; i++)
args[i] = Function.class;
args[args.length - numOps] = Types.raw(originalInfo.opType());
if (addCopyOp) args[args.length - 1] = Computers.Arity1.class;
return args;
}
private static Object[] constructorArgs( //
List> preconverters, //
Function, ?> postconverter, //
Computers.Arity1, ?> outputCopier, //
Object op //
) {
List