diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java index dadaa4dc..37baf4b5 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonClassTranslator.java @@ -2,6 +2,7 @@ import static ai.timefold.jpyinterpreter.PythonBytecodeToJavaBytecodeTranslator.ARGUMENT_SPEC_INSTANCE_FIELD_NAME; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -38,6 +39,7 @@ import ai.timefold.jpyinterpreter.types.BuiltinTypes; import ai.timefold.jpyinterpreter.types.CPythonBackedPythonLikeObject; import ai.timefold.jpyinterpreter.types.GeneratedFunctionMethodReference; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; import ai.timefold.jpyinterpreter.types.PythonLikeFunction; import ai.timefold.jpyinterpreter.types.PythonLikeType; import ai.timefold.jpyinterpreter.types.PythonNone; @@ -65,6 +67,7 @@ public class PythonClassTranslator { public static final String TYPE_FIELD_NAME = "$TYPE"; public static final String CPYTHON_TYPE_FIELD_NAME = "$CPYTHON_TYPE"; private static final String JAVA_METHOD_PREFIX = "$method$"; + private static final String PYTHON_JAVA_TYPE_MAPPING_PREFIX = "$pythonJavaTypeMapping"; public record PreparedClassInfo(PythonLikeType type, String className, String classInternalName) { } @@ -205,7 +208,25 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp } } + for (int i = 0; i < pythonCompiledClass.pythonJavaTypeMappings.size(); i++) { + classWriter.visitField(Modifier.PUBLIC | Modifier.STATIC, PYTHON_JAVA_TYPE_MAPPING_PREFIX + i, + Type.getDescriptor(PythonJavaTypeMapping.class), null, null); + } + Map attributeNameToTypeMap = new HashMap<>(); + instanceAttributeSet.removeAll(pythonCompiledClass.staticAttributeDescriptorNames); + try { + var parentClass = superClassType.getJavaClass(); + while (parentClass != Object.class) { + for (Field field : parentClass.getFields()) { + instanceAttributeSet.remove(field.getName()); + } + parentClass = parentClass.getSuperclass(); + } + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + for (String attributeName : instanceAttributeSet) { var typeHint = pythonCompiledClass.typeAnnotations.getOrDefault(attributeName, TypeHint.withoutAnnotations(BuiltinTypes.BASE_TYPE)); @@ -243,8 +264,8 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp } fieldVisitor.visitEnd(); - createJavaGetterSetter(classWriter, preparedClassInfo, - attributeName, + createJavaGetterSetter(classWriter, pythonCompiledClass, + preparedClassInfo, attributeName, Type.getType(javaFieldTypeDescriptor), Type.getType(getterTypeDescriptor), signature, @@ -368,6 +389,10 @@ public static PythonLikeType translatePythonClass(PythonCompiledClass pythonComp generatedClass = (Class) BuiltinTypes.asmClassLoader.loadClass(className); generatedClass.getField(TYPE_FIELD_NAME).set(null, pythonLikeType); generatedClass.getField(CPYTHON_TYPE_FIELD_NAME).set(null, pythonCompiledClass.binaryType); + for (int i = 0; i < pythonCompiledClass.pythonJavaTypeMappings.size(); i++) { + generatedClass.getField(PYTHON_JAVA_TYPE_MAPPING_PREFIX + i) + .set(null, pythonCompiledClass.pythonJavaTypeMappings.get(i)); + } } catch (ClassNotFoundException e) { throw new IllegalStateException("Impossible State: Unable to load generated class (" + className + ") despite it being just generated.", e); @@ -785,16 +810,31 @@ private static PythonLikeFunction createConstructor(String classInternalName, } } + private record MatchedMapping(int index, PythonJavaTypeMapping pythonJavaTypeMapping) { + } + private static void createJavaGetterSetter(ClassWriter classWriter, + PythonCompiledClass pythonCompiledClass, PreparedClassInfo preparedClassInfo, String attributeName, Type attributeType, Type getterType, String signature, TypeHint typeHint) { - createJavaGetter(classWriter, preparedClassInfo, attributeName, attributeType, getterType, signature, typeHint); - createJavaSetter(classWriter, preparedClassInfo, attributeName, attributeType, getterType, signature, typeHint); + MatchedMapping matchedMapping = null; + for (int i = 0; i < pythonCompiledClass.pythonJavaTypeMappings.size(); i++) { + var mapping = pythonCompiledClass.pythonJavaTypeMappings.get(i); + if (mapping.getPythonType().equals(typeHint.javaGetterType())) { + matchedMapping = new MatchedMapping(i, mapping); + getterType = Type.getType(mapping.getJavaType()); + } + } + createJavaGetter(classWriter, preparedClassInfo, matchedMapping, attributeName, attributeType, getterType, signature, + typeHint); + createJavaSetter(classWriter, preparedClassInfo, matchedMapping, attributeName, attributeType, getterType, signature, + typeHint); } - private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo preparedClassInfo, String attributeName, + private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo preparedClassInfo, + MatchedMapping matchedMapping, String attributeName, Type attributeType, Type getterType, String signature, TypeHint typeHint) { var getterName = "get" + attributeName.substring(0, 1).toUpperCase() + attributeName.substring(1); if (signature != null && Objects.equals(attributeType, getterType)) { @@ -826,6 +866,21 @@ private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo // If branch is not taken, stack is null } if (!Objects.equals(attributeType, getterType)) { + if (matchedMapping != null) { + getterVisitor.visitInsn(Opcodes.DUP); + getterVisitor.visitInsn(Opcodes.ACONST_NULL); + Label skipMapping = new Label(); + getterVisitor.visitJumpInsn(Opcodes.IF_ACMPEQ, skipMapping); + getterVisitor.visitFieldInsn(Opcodes.GETSTATIC, preparedClassInfo.classInternalName, + PYTHON_JAVA_TYPE_MAPPING_PREFIX + matchedMapping.index, + Type.getDescriptor(PythonJavaTypeMapping.class)); + getterVisitor.visitInsn(Opcodes.SWAP); + getterVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, + Type.getInternalName(PythonJavaTypeMapping.class), "toJavaObject", + Type.getMethodDescriptor(Type.getType(Object.class), Type.getType(Object.class)), + true); + getterVisitor.visitLabel(skipMapping); + } getterVisitor.visitTypeInsn(Opcodes.CHECKCAST, getterType.getInternalName()); } getterVisitor.visitInsn(Opcodes.ARETURN); @@ -833,7 +888,8 @@ private static void createJavaGetter(ClassWriter classWriter, PreparedClassInfo getterVisitor.visitEnd(); } - private static void createJavaSetter(ClassWriter classWriter, PreparedClassInfo preparedClassInfo, String attributeName, + private static void createJavaSetter(ClassWriter classWriter, PreparedClassInfo preparedClassInfo, + MatchedMapping matchedMapping, String attributeName, Type attributeType, Type setterType, String signature, TypeHint typeHint) { var setterName = "set" + attributeName.substring(0, 1).toUpperCase() + attributeName.substring(1); if (signature != null && Objects.equals(attributeType, setterType)) { @@ -861,6 +917,21 @@ private static void createJavaSetter(ClassWriter classWriter, PreparedClassInfo // If branch is not taken, stack is None } if (!Objects.equals(attributeType, setterType)) { + if (matchedMapping != null) { + setterVisitor.visitVarInsn(Opcodes.ALOAD, 1); + setterVisitor.visitInsn(Opcodes.ACONST_NULL); + Label skipMapping = new Label(); + setterVisitor.visitJumpInsn(Opcodes.IF_ACMPEQ, skipMapping); + setterVisitor.visitFieldInsn(Opcodes.GETSTATIC, preparedClassInfo.classInternalName, + PYTHON_JAVA_TYPE_MAPPING_PREFIX + matchedMapping.index, + Type.getDescriptor(PythonJavaTypeMapping.class)); + setterVisitor.visitInsn(Opcodes.SWAP); + setterVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, + Type.getInternalName(PythonJavaTypeMapping.class), "toPythonObject", + Type.getMethodDescriptor(Type.getType(Object.class), Type.getType(Object.class)), + true); + setterVisitor.visitLabel(skipMapping); + } setterVisitor.visitTypeInsn(Opcodes.CHECKCAST, attributeType.getInternalName()); } setterVisitor.visitFieldInsn(Opcodes.PUTFIELD, preparedClassInfo.classInternalName, diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java index 9210cfec..10cb7213 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonCompiledClass.java @@ -2,7 +2,9 @@ import java.util.List; import java.util.Map; +import java.util.Set; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; import ai.timefold.jpyinterpreter.types.PythonLikeType; import ai.timefold.jpyinterpreter.types.wrappers.CPythonType; import ai.timefold.jpyinterpreter.types.wrappers.OpaquePythonReference; @@ -41,6 +43,11 @@ public class PythonCompiledClass { */ public List> javaInterfaces; + /** + * Mapping from Python types to Java types + */ + public List> pythonJavaTypeMappings; + /** * The binary type of this PythonCompiledClass; * typically {@link CPythonType}. Used when methods @@ -62,6 +69,11 @@ public class PythonCompiledClass { */ public Map staticAttributeNameToClassInstance; + /** + * Contains static attributes that have get/set descriptors + */ + public Set staticAttributeDescriptorNames; + public String getGeneratedClassBaseName() { return getGeneratedClassBaseName(module, qualifiedName); } diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonLikeObject.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonLikeObject.java index 0391e30d..eb27c209 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonLikeObject.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/PythonLikeObject.java @@ -125,8 +125,8 @@ public interface PythonLikeObject { return PythonBoolean.valueOf(!Objects.equals(this, other)); } - default PythonLikeObject $method$__str__() { - return PythonString.valueOf(this.toString()); + default PythonString $method$__str__() { + return PythonString.valueOf(this.getClass().getSimpleName() + "@" + System.identityHashCode(this)); } default PythonLikeObject $method$__repr__() { diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/AbstractPythonLikeObject.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/AbstractPythonLikeObject.java index 1fec8da3..53e97f7f 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/AbstractPythonLikeObject.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/AbstractPythonLikeObject.java @@ -49,4 +49,9 @@ public AbstractPythonLikeObject(PythonLikeType __type__, Map { + PythonLikeType getPythonType(); + + Class getJavaType(); + + PythonType_ toPythonObject(JavaType_ javaObject); + + JavaType_ toJavaObject(PythonType_ pythonObject); +} diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonLikeType.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonLikeType.java index 5f94bd44..5e13521a 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonLikeType.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonLikeType.java @@ -598,6 +598,11 @@ public List getParentList() { return PARENT_TYPES; } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { return ""; diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonNone.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonNone.java index 0143c7ea..48c7103a 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonNone.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonNone.java @@ -30,6 +30,11 @@ private PythonNone() { super(BuiltinTypes.NONE_TYPE); } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { return "None"; diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonString.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonString.java index 5cf5a07c..9fe92d7f 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonString.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/PythonString.java @@ -1477,6 +1477,11 @@ public PythonString asString() { return this; } + @Override + public PythonString $method$__str__() { + return this; + } + @Override public String toString() { return value; diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeDict.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeDict.java index f146b1cf..1bd0115e 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeDict.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeDict.java @@ -402,6 +402,11 @@ public int hashCode() { return Objects.hash(delegate); } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { return delegate.toString(); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeList.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeList.java index 17d3a2be..c55e8dbc 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeList.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeList.java @@ -558,6 +558,11 @@ public List subList(int i, int i1) { return delegate.subList(i, i1); } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { StringBuilder out = new StringBuilder(); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeTuple.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeTuple.java index b1a723da..cd5ba5b8 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeTuple.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/collections/PythonLikeTuple.java @@ -20,6 +20,7 @@ import ai.timefold.jpyinterpreter.types.PythonLikeComparable; import ai.timefold.jpyinterpreter.types.PythonLikeType; import ai.timefold.jpyinterpreter.types.PythonSlice; +import ai.timefold.jpyinterpreter.types.PythonString; import ai.timefold.jpyinterpreter.types.errors.TypeError; import ai.timefold.jpyinterpreter.types.errors.ValueError; import ai.timefold.jpyinterpreter.types.errors.lookup.IndexError; @@ -444,6 +445,11 @@ public int hashCode() { return PythonInteger.valueOf(hashCode()); } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { return delegate.toString(); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTime.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTime.java index d6194970..8e35a74e 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTime.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTime.java @@ -328,8 +328,9 @@ public PythonString isoformat(PythonString formatSpec) { return PythonString.valueOf(result); } - public PythonString toPythonString() { - return new PythonString(localTime.toString()); + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); } @Override diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTimeDelta.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTimeDelta.java index 97c0e288..5b6fb0bb 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTimeDelta.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/datetime/PythonTimeDelta.java @@ -339,6 +339,11 @@ public PythonBoolean isZero() { return PythonBoolean.valueOf(duration.isZero()); } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { StringBuilder out = new StringBuilder(); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonBoolean.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonBoolean.java index 41baaa85..ca173a2a 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonBoolean.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonBoolean.java @@ -123,6 +123,11 @@ public static PythonBoolean valueOf(boolean result) { return BuiltinTypes.BOOLEAN_TYPE; } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { if (this == TRUE) { diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonFloat.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonFloat.java index b3ddc941..43a13f2c 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonFloat.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonFloat.java @@ -206,6 +206,11 @@ public PythonLikeTuple asFraction() { return PythonLikeTuple.fromItems(PythonInteger.valueOf(numerator), PythonInteger.valueOf(denominator)); } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { return Double.toString(value); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonInteger.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonInteger.java index c0462d0f..afa2155b 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonInteger.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/numeric/PythonInteger.java @@ -237,6 +237,11 @@ public Number getValue() { return value; } + @Override + public PythonString $method$__str__() { + return PythonString.valueOf(toString()); + } + @Override public String toString() { return value.toString(); diff --git a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/wrappers/PythonLikeFunctionWrapper.java b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/wrappers/PythonLikeFunctionWrapper.java index a0cf51fc..8b01ea93 100644 --- a/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/wrappers/PythonLikeFunctionWrapper.java +++ b/jpyinterpreter/src/main/java/ai/timefold/jpyinterpreter/types/wrappers/PythonLikeFunctionWrapper.java @@ -73,7 +73,7 @@ public void setWrapped(PythonLikeFunction wrapped) { return wrapped.$method$__ne__(other); } - public PythonLikeObject $method$__str__() { + public PythonString $method$__str__() { return wrapped.$method$__str__(); } diff --git a/jpyinterpreter/src/main/python/__init__.py b/jpyinterpreter/src/main/python/__init__.py index 61e8114d..993e720e 100644 --- a/jpyinterpreter/src/main/python/__init__.py +++ b/jpyinterpreter/src/main/python/__init__.py @@ -4,7 +4,7 @@ from .jvm_setup import init, set_class_output_directory from .annotations import JavaAnnotation, AnnotationValueSupplier, add_class_annotation, add_java_interface from .conversions import (convert_to_java_python_like_object, unwrap_python_like_object, - update_python_object_from_java, is_c_native) + update_python_object_from_java, is_c_native, add_python_java_type_mapping) from .translator import (translate_python_bytecode_to_java_bytecode, translate_python_class_to_java_class, force_update_type, diff --git a/jpyinterpreter/src/main/python/annotations.py b/jpyinterpreter/src/main/python/annotations.py index abff81a8..5bdac088 100644 --- a/jpyinterpreter/src/main/python/annotations.py +++ b/jpyinterpreter/src/main/python/annotations.py @@ -57,6 +57,7 @@ def copy_type_annotations(hinted_object, default_args, vargs_name, kwargs_name): from java.util import HashMap, Collections from ai.timefold.jpyinterpreter import TypeHint from .translator import type_to_compiled_java_class + from typing import ClassVar out = HashMap() try: @@ -81,6 +82,11 @@ def copy_type_annotations(hinted_object, default_args, vargs_name, kwargs_name): hint_type = get_args(type_hint)[0] hint_annotations = get_java_annotations(type_hint.__metadata__) # noqa + if get_origin(type_hint) is ClassVar: + # Skip over class variables, since they are not + # instance attributes + continue + if name in default_args: hint_type = Union[hint_type, type(default_args[name])] diff --git a/jpyinterpreter/src/main/python/conversions.py b/jpyinterpreter/src/main/python/conversions.py index 60224611..274ec561 100644 --- a/jpyinterpreter/src/main/python/conversions.py +++ b/jpyinterpreter/src/main/python/conversions.py @@ -173,6 +173,11 @@ def init_type_to_compiled_java_class(): pass +def add_python_java_type_mapping(mapping): + from .translator import python_java_type_mappings + python_java_type_mappings.append(mapping) + + def copy_iterable(iterable): from java.util import ArrayList if iterable is None: diff --git a/jpyinterpreter/src/main/python/translator.py b/jpyinterpreter/src/main/python/translator.py index b384b0ae..3783f4c9 100644 --- a/jpyinterpreter/src/main/python/translator.py +++ b/jpyinterpreter/src/main/python/translator.py @@ -16,6 +16,7 @@ type_to_compiled_java_class = dict() type_to_annotations = dict() type_to_java_interfaces = dict() +python_java_type_mappings = list() function_interface_pair_to_instance = dict() function_interface_pair_to_class = dict() @@ -506,6 +507,59 @@ def force_update_type(python_type, java_type): type_to_compiled_java_class[python_type] = java_type +# TODO: Remove me when minimum Python version is 3.11 +def get_members_static(object, predicate): + try: + return inspect.getmembers_static(object, predicate) + except AttributeError: + return _getmembers(object, predicate, type.__getattribute__) + + +# TODO: Remove me when minimum Python version is 3.11 +def _getmembers(object, predicate, getter): + import types + results = [] + processed = set() + names = dir(object) + if inspect.isclass(object): + mro = (object,) + inspect.getmro(object) + # add any DynamicClassAttributes to the list of names if object is a class; + # this may result in duplicate entries if, for example, a virtual + # attribute with the same name as a DynamicClassAttribute exists + try: + for base in object.__bases__: + for k, v in base.__dict__.items(): + if isinstance(v, types.DynamicClassAttribute): + names.append(k) + except AttributeError: + pass + else: + mro = () + for key in names: + # First try to get the value via getattr. Some descriptors don't + # like calling their __get__ (see bug #1785), so fall back to + # looking in the __dict__. + try: + value = getter(object, key) + # handle the duplicate key + if key in processed: + raise AttributeError + except AttributeError: + for base in mro: + if key in base.__dict__: + value = base.__dict__[key] + break + else: + # could be a (currently) missing slot member, or a buggy + # __dir__; discard and move on + continue + if not predicate or predicate(value): + results.append((key, value)) + processed.add(key) + results.sort(key=lambda pair: pair[0]) + return results + + def translate_python_class_to_java_class(python_class): import collections.abc as collections_abc from .annotations import erase_generic_args, convert_java_annotation, copy_type_annotations @@ -513,7 +567,7 @@ def translate_python_class_to_java_class(python_class): init_type_to_compiled_java_class, is_banned_module, is_c_native, convert_to_java_python_like_object ) from java.lang import Class as JavaClass - from java.util import ArrayList, HashMap + from java.util import ArrayList, HashMap, HashSet from ai.timefold.jpyinterpreter import AnnotationMetadata, PythonCompiledClass, PythonClassTranslator, CPythonBackedPythonInterpreter # noqa from ai.timefold.jpyinterpreter.types import BuiltinTypes from ai.timefold.jpyinterpreter.types.wrappers import JavaObjectWrapper, OpaquePythonReference, CPythonType # noqa @@ -575,10 +629,13 @@ def translate_python_class_to_java_class(python_class): isinstance(method, __CLASS_METHOD_TYPE): methods.append((method_name, method)) - static_attributes = inspect.getmembers(python_class, predicate=lambda member: not (inspect.isfunction(member) - or isinstance(member, __STATIC_METHOD_TYPE) - or isinstance(member, __CLASS_METHOD_TYPE))) - static_attributes = [attribute for attribute in static_attributes if attribute[0] in python_class.__dict__] + all_static_attributes = get_members_static(python_class, + predicate=lambda member: not (inspect.isfunction(member) + or isinstance(member, + __STATIC_METHOD_TYPE) + or isinstance(member, __CLASS_METHOD_TYPE) + )) + static_attributes = [attribute for attribute in all_static_attributes if attribute[0] in python_class.__dict__] static_methods = [method for method in methods if isinstance(method[1], __STATIC_METHOD_TYPE)] class_methods = [method for method in methods if isinstance(method[1], __CLASS_METHOD_TYPE)] instance_methods = [method for method in methods if method not in static_methods and method not in class_methods] @@ -620,6 +677,10 @@ def translate_python_class_to_java_class(python_class): static_attributes_map = HashMap() static_attributes_to_class_instance_map = HashMap() + static_attribute_descriptor_names = HashSet() + static_attribute_descriptor_names.add('__class__') + static_attribute_descriptor_names.add('__module__') + for attribute in static_attributes: attribute_type = type(attribute[1]) if attribute_type == python_class: @@ -636,9 +697,16 @@ def translate_python_class_to_java_class(python_class): static_attributes_map.put(attribute[0], convert_to_java_python_like_object(attribute[1])) + for attribute in all_static_attributes: + attribute_type = type(attribute[1]) + if (hasattr(attribute_type, '__get__') or hasattr(attribute_type, '__set__') or + hasattr(attribute[1], '__get__') or hasattr(attribute[1], '__set__')): + static_attribute_descriptor_names.add(attribute[0]) + python_compiled_class = PythonCompiledClass() python_compiled_class.annotations = ArrayList() python_compiled_class.javaInterfaces = ArrayList() + python_compiled_class.pythonJavaTypeMappings = ArrayList() for annotation in type_to_annotations.get(python_class, []): python_compiled_class.annotations.add(convert_java_annotation(annotation)) @@ -649,6 +717,9 @@ def translate_python_class_to_java_class(python_class): python_compiled_class.javaInterfaces.add(java_interface) + for python_java_type_mapping in python_java_type_mappings: + python_compiled_class.pythonJavaTypeMappings.add(python_java_type_mapping) + python_compiled_class.binaryType = CPythonType.getType(JProxy(OpaquePythonReference, inst=python_class, convert=True)) python_compiled_class.module = python_class.__module__ @@ -665,6 +736,7 @@ def translate_python_class_to_java_class(python_class): python_compiled_class.classFunctionNameToPythonBytecode = class_method_map python_compiled_class.staticAttributeNameToObject = static_attributes_map python_compiled_class.staticAttributeNameToClassInstance = static_attributes_to_class_instance_map + python_compiled_class.staticAttributeDescriptorNames = static_attribute_descriptor_names out = PythonClassTranslator.translatePythonClass(python_compiled_class, prepared_class_info) PythonClassTranslator.setSelfStaticInstances(python_compiled_class, out.getJavaClass(), out, diff --git a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/PythonClassTranslatorTest.java b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/PythonClassTranslatorTest.java index 6148c399..6fd1b237 100644 --- a/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/PythonClassTranslatorTest.java +++ b/jpyinterpreter/src/test/java/ai/timefold/jpyinterpreter/PythonClassTranslatorTest.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.function.ToIntFunction; @@ -47,10 +48,12 @@ public void testPythonClassTranslation() throws ClassNotFoundException, NoSuchMe compiledClass.annotations = Collections.emptyList(); compiledClass.javaInterfaces = Collections.emptyList(); + compiledClass.pythonJavaTypeMappings = List.of(); compiledClass.className = "MyClass"; compiledClass.superclassList = List.of(BuiltinTypes.BASE_TYPE); compiledClass.staticAttributeNameToObject = Map.of("type_variable", new PythonString("type_value")); compiledClass.staticAttributeNameToClassInstance = Map.of(); + compiledClass.staticAttributeDescriptorNames = Set.of(); compiledClass.typeAnnotations = Map.of("age", TypeHint.withoutAnnotations(BuiltinTypes.INT_TYPE)); compiledClass.instanceFunctionNameToPythonBytecode = Map.of("__init__", initFunction, "get_age", ageFunction); @@ -101,10 +104,12 @@ public void testPythonClassComparable() throws ClassNotFoundException { PythonCompiledClass compiledClass = new PythonCompiledClass(); compiledClass.annotations = Collections.emptyList(); compiledClass.javaInterfaces = Collections.emptyList(); + compiledClass.pythonJavaTypeMappings = List.of(); compiledClass.className = "MyClass"; compiledClass.superclassList = List.of(BuiltinTypes.BASE_TYPE); compiledClass.staticAttributeNameToObject = Map.of(); compiledClass.staticAttributeNameToClassInstance = Map.of(); + compiledClass.staticAttributeDescriptorNames = Set.of(); compiledClass.typeAnnotations = Map.of("key", TypeHint.withoutAnnotations(BuiltinTypes.INT_TYPE)); compiledClass.instanceFunctionNameToPythonBytecode = Map.of("__init__", initFunction, compareOp.dunderMethod, comparisonFunction); @@ -171,10 +176,12 @@ public void testPythonClassEqualsAndHashCode() throws ClassNotFoundException { PythonCompiledClass compiledClass = new PythonCompiledClass(); compiledClass.annotations = Collections.emptyList(); compiledClass.javaInterfaces = Collections.emptyList(); + compiledClass.pythonJavaTypeMappings = List.of(); compiledClass.className = "MyClass"; compiledClass.superclassList = List.of(BuiltinTypes.BASE_TYPE); compiledClass.staticAttributeNameToObject = Map.of(); compiledClass.staticAttributeNameToClassInstance = Map.of(); + compiledClass.staticAttributeDescriptorNames = Set.of(); compiledClass.typeAnnotations = Map.of("key", TypeHint.withoutAnnotations(BuiltinTypes.INT_TYPE)); compiledClass.instanceFunctionNameToPythonBytecode = Map.of("__init__", initFunction, "__eq__", equalsFunction, @@ -238,10 +245,12 @@ public void testPythonClassSimpleInterface() throws ClassNotFoundException { PythonCompiledClass compiledClass = new PythonCompiledClass(); compiledClass.annotations = Collections.emptyList(); compiledClass.javaInterfaces = List.of(ToIntFunction.class); + compiledClass.pythonJavaTypeMappings = List.of(); compiledClass.className = "MyClass"; compiledClass.superclassList = List.of(BuiltinTypes.BASE_TYPE); compiledClass.staticAttributeNameToObject = Map.of(); compiledClass.staticAttributeNameToClassInstance = Map.of(); + compiledClass.staticAttributeDescriptorNames = Set.of(); compiledClass.typeAnnotations = Map.of("key", TypeHint.withoutAnnotations(BuiltinTypes.INT_TYPE)); compiledClass.instanceFunctionNameToPythonBytecode = Map.of("__init__", initFunction, "applyAsInt", applyAsInt); @@ -301,10 +310,12 @@ public void testPythonClassComplexInterface() throws ClassNotFoundException { PythonCompiledClass compiledClass = new PythonCompiledClass(); compiledClass.annotations = Collections.emptyList(); compiledClass.javaInterfaces = List.of(ComplexInterface.class); + compiledClass.pythonJavaTypeMappings = List.of(); compiledClass.className = "MyClass"; compiledClass.superclassList = List.of(BuiltinTypes.BASE_TYPE); compiledClass.staticAttributeNameToObject = Map.of(); compiledClass.staticAttributeNameToClassInstance = Map.of(); + compiledClass.staticAttributeDescriptorNames = Set.of(); compiledClass.typeAnnotations = Map.of("key", TypeHint.withoutAnnotations(BuiltinTypes.INT_TYPE)); compiledClass.instanceFunctionNameToPythonBytecode = Map.of("__init__", initFunction, "overloadedMethod", overloadedMethod); diff --git a/jpyinterpreter/tests/test_classes.py b/jpyinterpreter/tests/test_classes.py index e887ae7b..5819d071 100644 --- a/jpyinterpreter/tests/test_classes.py +++ b/jpyinterpreter/tests/test_classes.py @@ -1,4 +1,7 @@ from typing import Type + +import pytest + from .conftest import verifier_for @@ -1013,6 +1016,7 @@ class A: def test_functional_interface(): from java.util.function import ToIntFunction from jpyinterpreter import translate_python_class_to_java_class, add_java_interface + from ai.timefold.jpyinterpreter.types import PythonNone @add_java_interface(ToIntFunction) class A: @@ -1023,3 +1027,86 @@ def applyAsInt(self, argument: int): assert ToIntFunction.class_.isAssignableFrom(translated_class) java_object = translated_class.getConstructor().newInstance() assert java_object.applyAsInt(1) == 2 + + +def test_python_java_type_mapping(): + from java.lang import String + from jpyinterpreter import (translate_python_class_to_java_class, + add_python_java_type_mapping, unwrap_python_like_object) + from jpype import JImplements, JOverride + from dataclasses import dataclass + + @dataclass + class PythonClass: + data: str + + python_class_type = translate_python_class_to_java_class(PythonClass) + + @JImplements('ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping') + class MyMapping: + @JOverride + def getPythonType(self): + return python_class_type + + @JOverride + def getJavaType(self): + return String.class_ + + @JOverride + def toPythonObject(self, java_object): + from ai.timefold.jpyinterpreter.types import PythonString + instance = python_class_type.getJavaClass().getConstructor().newInstance() + instance.data = PythonString.valueOf(java_object) + return instance + + @JOverride + def toJavaObject(self, python_object): + return python_object.data.getValue() + + add_python_java_type_mapping(MyMapping()) + + @dataclass + class A: + data: PythonClass | None + + translated_class = translate_python_class_to_java_class(A).getJavaClass() + assert translated_class.getMethod('getData').getReturnType() == String.class_ + assert translated_class.getMethod('setData', String.class_) is not None + + java_object = translated_class.getConstructor().newInstance() + java_object.setData('test') + assert unwrap_python_like_object(translated_class.getField('data').get(java_object)) == PythonClass('test') + assert java_object.getData() == 'test' + + java_object.setData(None) + assert unwrap_python_like_object(translated_class.getField('data').get(java_object)) is None + assert java_object.getData() is None + + +def test_class_properties(): + from jpyinterpreter import translate_python_class_to_java_class, unwrap_python_like_object + from dataclasses import dataclass + from java.lang import NoSuchFieldException + from ai.timefold.jpyinterpreter.types import PythonString + + @dataclass + class Car: + name: str + + @property + def speed(self): + return 100 + + def is_fast(self) -> bool: + return self.speed > 50 + + translated_class = translate_python_class_to_java_class(Car) + java_class = translated_class.getJavaClass() + + with pytest.raises(NoSuchFieldException): + java_class.getField('speed') + + instance = java_class.getConstructor().newInstance() + instance.name = PythonString.valueOf('Car') + assert java_class.getMethod('$method$is_fast').invoke(instance) + assert unwrap_python_like_object(instance) == Car('Car') diff --git a/tests/test_domain.py b/tests/test_domain.py index a99277e4..690af30e 100644 --- a/tests/test_domain.py +++ b/tests/test_domain.py @@ -341,8 +341,8 @@ def my_constraints(constraint_factory: ConstraintFactory): solver = SolverFactory.create(solver_config).build_solver() solution = solver.solve(problem) - assert solution.score.getHardScore() == 0 - assert solution.score.getSoftScore() == 0 + assert solution.score.hard_score == 0 + assert solution.score.soft_score == 0 assert solution.entity_list[0].value == v2 assert solution.entity_list[1].value == v1 diff --git a/tests/test_incremental_score_calculator.py b/tests/test_incremental_score_calculator.py index 8d2959c7..d648aaa3 100644 --- a/tests/test_incremental_score_calculator.py +++ b/tests/test_incremental_score_calculator.py @@ -113,7 +113,7 @@ def retract(self, queen: Queen): descending_diagonal_index_list.remove(queen) self.score += len(descending_diagonal_index_list) - def calculate_score(self) -> HardSoftScore: + def calculate_score(self) -> SimpleScore: return SimpleScore.of(self.score) solver_config = SolverConfig( @@ -216,7 +216,7 @@ def retract(self, queen: Queen): descending_diagonal_index_list.remove(queen) self.score += len(descending_diagonal_index_list) - def calculate_score(self) -> HardSoftScore: + def calculate_score(self) -> SimpleScore: return SimpleScore.of(self.score) def get_constraint_match_totals(self): diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/BendableScorePythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/BendableScorePythonJavaTypeMapping.java new file mode 100644 index 00000000..82fe8218 --- /dev/null +++ b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/BendableScorePythonJavaTypeMapping.java @@ -0,0 +1,85 @@ +package ai.timefold.solver.python.score; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +import ai.timefold.jpyinterpreter.PythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.collections.PythonLikeTuple; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.bendable.BendableScore; + +public final class BendableScorePythonJavaTypeMapping implements PythonJavaTypeMapping { + private final PythonLikeType type; + private final Constructor constructor; + private final Field initScoreField; + private final Field hardScoresField; + private final Field softScoresField; + + public BendableScorePythonJavaTypeMapping(PythonLikeType type) + throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { + this.type = type; + Class clazz = type.getJavaClass(); + constructor = clazz.getConstructor(); + initScoreField = clazz.getField("init_score"); + hardScoresField = clazz.getField("hard_scores"); + softScoresField = clazz.getField("soft_scores"); + } + + @Override + public PythonLikeType getPythonType() { + return type; + } + + @Override + public Class getJavaType() { + return BendableScore.class; + } + + private static PythonLikeTuple toPythonList(int[] scores) { + PythonLikeTuple out = new PythonLikeTuple<>(); + for (int score : scores) { + out.add(PythonInteger.valueOf(score)); + } + return out; + } + + @Override + public PythonLikeObject toPythonObject(BendableScore javaObject) { + try { + var instance = constructor.newInstance(); + initScoreField.set(instance, PythonInteger.valueOf(javaObject.initScore())); + hardScoresField.set(instance, toPythonList(javaObject.hardScores())); + softScoresField.set(instance, toPythonList(javaObject.softScores())); + return (PythonLikeObject) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public BendableScore toJavaObject(PythonLikeObject pythonObject) { + try { + var initScore = ((PythonInteger) initScoreField.get(pythonObject)).value.intValue(); + var hardScoreTuple = ((PythonLikeTuple) hardScoresField.get(pythonObject)); + var softScoreTuple = ((PythonLikeTuple) softScoresField.get(pythonObject)); + int[] hardScores = new int[hardScoreTuple.size()]; + int[] softScores = new int[softScoreTuple.size()]; + for (int i = 0; i < hardScores.length; i++) { + hardScores[i] = ((PythonInteger) hardScoreTuple.get(i)).value.intValue(); + } + for (int i = 0; i < softScores.length; i++) { + softScores[i] = ((PythonInteger) softScoreTuple.get(i)).value.intValue(); + } + if (initScore == 0) { + return BendableScore.of(hardScores, softScores); + } else { + return BendableScore.ofUninitialized(initScore, hardScores, softScores); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/HardMediumSoftScorePythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/HardMediumSoftScorePythonJavaTypeMapping.java new file mode 100644 index 00000000..98ebe894 --- /dev/null +++ b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/HardMediumSoftScorePythonJavaTypeMapping.java @@ -0,0 +1,73 @@ +package ai.timefold.solver.python.score; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +import ai.timefold.jpyinterpreter.PythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; + +public final class HardMediumSoftScorePythonJavaTypeMapping + implements PythonJavaTypeMapping { + private final PythonLikeType type; + private final Constructor constructor; + private final Field initScoreField; + private final Field hardScoreField; + private final Field mediumScoreField; + private final Field softScoreField; + + public HardMediumSoftScorePythonJavaTypeMapping(PythonLikeType type) + throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { + this.type = type; + Class clazz = type.getJavaClass(); + constructor = clazz.getConstructor(); + initScoreField = clazz.getField("init_score"); + hardScoreField = clazz.getField("hard_score"); + mediumScoreField = clazz.getField("medium_score"); + softScoreField = clazz.getField("soft_score"); + } + + @Override + public PythonLikeType getPythonType() { + return type; + } + + @Override + public Class getJavaType() { + return HardMediumSoftScore.class; + } + + @Override + public PythonLikeObject toPythonObject(HardMediumSoftScore javaObject) { + try { + var instance = constructor.newInstance(); + initScoreField.set(instance, PythonInteger.valueOf(javaObject.initScore())); + hardScoreField.set(instance, PythonInteger.valueOf(javaObject.hardScore())); + mediumScoreField.set(instance, PythonInteger.valueOf(javaObject.mediumScore())); + softScoreField.set(instance, PythonInteger.valueOf(javaObject.softScore())); + return (PythonLikeObject) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public HardMediumSoftScore toJavaObject(PythonLikeObject pythonObject) { + try { + var initScore = ((PythonInteger) initScoreField.get(pythonObject)).value.intValue(); + var hardScore = ((PythonInteger) hardScoreField.get(pythonObject)).value.intValue(); + var mediumScore = ((PythonInteger) mediumScoreField.get(pythonObject)).value.intValue(); + var softScore = ((PythonInteger) softScoreField.get(pythonObject)).value.intValue(); + if (initScore == 0) { + return HardMediumSoftScore.of(hardScore, mediumScore, softScore); + } else { + return HardMediumSoftScore.ofUninitialized(initScore, hardScore, mediumScore, softScore); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/HardSoftScorePythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/HardSoftScorePythonJavaTypeMapping.java new file mode 100644 index 00000000..4ef2efb7 --- /dev/null +++ b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/HardSoftScorePythonJavaTypeMapping.java @@ -0,0 +1,68 @@ +package ai.timefold.solver.python.score; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +import ai.timefold.jpyinterpreter.PythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; + +public final class HardSoftScorePythonJavaTypeMapping implements PythonJavaTypeMapping { + private final PythonLikeType type; + private final Constructor constructor; + private final Field initScoreField; + private final Field hardScoreField; + private final Field softScoreField; + + public HardSoftScorePythonJavaTypeMapping(PythonLikeType type) + throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { + this.type = type; + Class clazz = type.getJavaClass(); + constructor = clazz.getConstructor(); + initScoreField = clazz.getField("init_score"); + hardScoreField = clazz.getField("hard_score"); + softScoreField = clazz.getField("soft_score"); + } + + @Override + public PythonLikeType getPythonType() { + return type; + } + + @Override + public Class getJavaType() { + return HardSoftScore.class; + } + + @Override + public PythonLikeObject toPythonObject(HardSoftScore javaObject) { + try { + var instance = constructor.newInstance(); + initScoreField.set(instance, PythonInteger.valueOf(javaObject.initScore())); + hardScoreField.set(instance, PythonInteger.valueOf(javaObject.hardScore())); + softScoreField.set(instance, PythonInteger.valueOf(javaObject.softScore())); + return (PythonLikeObject) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public HardSoftScore toJavaObject(PythonLikeObject pythonObject) { + try { + var initScore = ((PythonInteger) initScoreField.get(pythonObject)).value.intValue(); + var hardScore = ((PythonInteger) hardScoreField.get(pythonObject)).value.intValue(); + var softScore = ((PythonInteger) softScoreField.get(pythonObject)).value.intValue(); + if (initScore == 0) { + return HardSoftScore.of(hardScore, softScore); + } else { + return HardSoftScore.ofUninitialized(initScore, hardScore, softScore); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/SimpleScorePythonJavaTypeMapping.java b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/SimpleScorePythonJavaTypeMapping.java new file mode 100644 index 00000000..40f3fcdb --- /dev/null +++ b/timefold-solver-python-core/src/main/java/ai/timefold/solver/python/score/SimpleScorePythonJavaTypeMapping.java @@ -0,0 +1,64 @@ +package ai.timefold.solver.python.score; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +import ai.timefold.jpyinterpreter.PythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonJavaTypeMapping; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; + +public final class SimpleScorePythonJavaTypeMapping implements PythonJavaTypeMapping { + private final PythonLikeType type; + private final Constructor constructor; + private final Field initScoreField; + private final Field scoreField; + + public SimpleScorePythonJavaTypeMapping(PythonLikeType type) + throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException { + this.type = type; + Class clazz = type.getJavaClass(); + constructor = clazz.getConstructor(); + initScoreField = clazz.getField("init_score"); + scoreField = clazz.getField("score"); + } + + @Override + public PythonLikeType getPythonType() { + return type; + } + + @Override + public Class getJavaType() { + return SimpleScore.class; + } + + @Override + public PythonLikeObject toPythonObject(SimpleScore javaObject) { + try { + var instance = constructor.newInstance(); + initScoreField.set(instance, PythonInteger.valueOf(javaObject.initScore())); + scoreField.set(instance, PythonInteger.valueOf(javaObject.score())); + return (PythonLikeObject) instance; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public SimpleScore toJavaObject(PythonLikeObject pythonObject) { + try { + var initScore = ((PythonInteger) initScoreField.get(pythonObject)).value.intValue(); + var score = ((PythonInteger) scoreField.get(pythonObject)).value.intValue(); + if (initScore == 0) { + return SimpleScore.of(score); + } else { + return SimpleScore.ofUninitialized(initScore, score); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/timefold-solver-python-core/src/main/python/_jpype_type_conversions.py b/timefold-solver-python-core/src/main/python/_jpype_type_conversions.py index 74bc5114..c78c8f58 100644 --- a/timefold-solver-python-core/src/main/python/_jpype_type_conversions.py +++ b/timefold-solver-python-core/src/main/python/_jpype_type_conversions.py @@ -1,6 +1,11 @@ from jpype import JProxy, JImplements, JOverride, JConversion from jpype.types import * from types import FunctionType +from typing import TYPE_CHECKING +import timefold.solver._timefold_java_interop as _timefold_java_interop + +if TYPE_CHECKING: + from .score._score import Score @JImplements('ai.timefold.solver.core.api.score.stream.ConstraintProvider', deferred=True) @@ -184,6 +189,27 @@ def test(self, argument1, argument2, argument3, argument4, argument5): return self.delegate(argument1, argument2, argument3, argument4, argument5) +def to_python_score(score) -> 'Score': + if isinstance(score, _timefold_java_interop._java_score_mapping_dict['SimpleScore']): + return _timefold_java_interop._python_score_mapping_dict['SimpleScore'](score.score(), + init_score=score.initScore()) + elif isinstance(score, _timefold_java_interop._java_score_mapping_dict['HardSoftScore']): + return _timefold_java_interop._python_score_mapping_dict['HardSoftScore'](score.hardScore(), + score.softScore(), + init_score=score.initScore()) + elif isinstance(score, _timefold_java_interop._java_score_mapping_dict['HardMediumSoftScore']): + return _timefold_java_interop._python_score_mapping_dict['HardMediumSoftScore'](score.hardScore(), + score.mediumScore(), + score.softScore(), + init_score=score.initScore()) + elif isinstance(score, _timefold_java_interop._java_score_mapping_dict['BendableScore']): + return _timefold_java_interop._python_score_mapping_dict['BendableScore'](score.hardScores(), + score.softScores(), + init_score=score.initScore()) + else: + raise TypeError(f'Unexpected score type: {type(score)}') + + # Function convertors def _has_java_class(item): if isinstance(item, (JObject, int, str, bool)): diff --git a/timefold-solver-python-core/src/main/python/_solution_manager.py b/timefold-solver-python-core/src/main/python/_solution_manager.py index c8544c07..cbe24696 100644 --- a/timefold-solver-python-core/src/main/python/_solution_manager.py +++ b/timefold-solver-python-core/src/main/python/_solution_manager.py @@ -1,5 +1,6 @@ from ._solver_factory import SolverFactory from ._solver_manager import SolverManager +from ._jpype_type_conversions import to_python_score from .score import ScoreAnalysis, ScoreExplanation from typing import TypeVar, Generic, TYPE_CHECKING, Any @@ -63,7 +64,7 @@ def update(self, solution: Solution_, solution_update_policy=None) -> 'Score': java_solution = convert_to_java_python_like_object(solution) out = self._delegate.update(java_solution) update_python_object_from_java(java_solution) - return out + return to_python_score(out) def analyze(self, solution: Solution_, score_analysis_fetch_policy=None, solution_update_policy=None) \ -> 'ScoreAnalysis': diff --git a/timefold-solver-python-core/src/main/python/_solver.py b/timefold-solver-python-core/src/main/python/_solver.py index fb011c60..fd3b6473 100644 --- a/timefold-solver-python-core/src/main/python/_solver.py +++ b/timefold-solver-python-core/src/main/python/_solver.py @@ -1,5 +1,6 @@ from ._problem_change import ProblemChange, ProblemChangeWrapper from ._timefold_java_interop import update_log_level +from ._jpype_type_conversions import to_python_score from typing import TypeVar, TYPE_CHECKING, Generic, Callable from datetime import timedelta from jpype import JClass, JImplements, JOverride @@ -234,7 +235,7 @@ def bestSolutionChanged(self, event): from _jpyinterpreter import unwrap_python_like_object nonlocal event_listener_list event = BestSolutionChangedEvent( - new_best_score=event.getNewBestScore(), + new_best_score=to_python_score(event.getNewBestScore()), new_best_solution=unwrap_python_like_object(event.getNewBestSolution()), is_every_problem_change_processed=event.isEveryProblemChangeProcessed(), time_spent=timedelta(milliseconds=event.getTimeMillisSpent()) diff --git a/timefold-solver-python-core/src/main/python/_timefold_java_interop.py b/timefold-solver-python-core/src/main/python/_timefold_java_interop.py index 4da32581..0d5a9f85 100644 --- a/timefold-solver-python-core/src/main/python/_timefold_java_interop.py +++ b/timefold-solver-python-core/src/main/python/_timefold_java_interop.py @@ -22,6 +22,9 @@ _compilation_queue: list[type] = [] _enterprise_installed: bool = False +_scores_registered: bool = False +_python_score_mapping_dict: dict[str, object] = {} +_java_score_mapping_dict: dict[str, object] = {} def is_enterprise_installed() -> bool: @@ -94,6 +97,46 @@ def update_log_level() -> None: PythonLoggingToLogbackAdapter.setLevel(logger.getEffectiveLevel()) +def register_score_python_java_type_mappings(): + global _scores_registered, _java_score_mapping_dict, _python_score_mapping_dict + if _scores_registered: + return + + _scores_registered = True + + from .score._score import SimpleScore, HardSoftScore, HardMediumSoftScore, BendableScore + from ai.timefold.solver.core.api.score.buildin.simple import SimpleScore as _SimpleScore + from ai.timefold.solver.core.api.score.buildin.hardsoft import HardSoftScore as _HardSoftScore + from ai.timefold.solver.core.api.score.buildin.hardmediumsoft import HardMediumSoftScore as _HardMediumSoftScore + from ai.timefold.solver.core.api.score.buildin.bendable import BendableScore as _BendableScore + + from ai.timefold.solver.python.score import (SimpleScorePythonJavaTypeMapping, + HardSoftScorePythonJavaTypeMapping, + HardMediumSoftScorePythonJavaTypeMapping, + BendableScorePythonJavaTypeMapping) + from _jpyinterpreter import translate_python_class_to_java_class, add_python_java_type_mapping + + _python_score_mapping_dict['SimpleScore'] = SimpleScore + _python_score_mapping_dict['HardSoftScore'] = HardSoftScore + _python_score_mapping_dict['HardMediumSoftScore'] = HardMediumSoftScore + _python_score_mapping_dict['BendableScore'] = BendableScore + + _java_score_mapping_dict['SimpleScore'] = _SimpleScore + _java_score_mapping_dict['HardSoftScore'] = _HardSoftScore + _java_score_mapping_dict['HardMediumSoftScore'] = _HardMediumSoftScore + _java_score_mapping_dict['BendableScore'] = _BendableScore + + SimpleScoreType = translate_python_class_to_java_class(SimpleScore) + HardSoftScoreType = translate_python_class_to_java_class(HardSoftScore) + HardMediumSoftScoreType = translate_python_class_to_java_class(HardMediumSoftScore) + BendableScoreType = translate_python_class_to_java_class(BendableScore) + + add_python_java_type_mapping(SimpleScorePythonJavaTypeMapping(SimpleScoreType)) + add_python_java_type_mapping(HardSoftScorePythonJavaTypeMapping(HardSoftScoreType)) + add_python_java_type_mapping(HardMediumSoftScorePythonJavaTypeMapping(HardMediumSoftScoreType)) + add_python_java_type_mapping(BendableScorePythonJavaTypeMapping(BendableScoreType)) + + def forward_logging_events(event: 'PythonLoggingEvent') -> None: logger.log(event.level().getPythonLevelNumber(), event.message()) @@ -257,6 +300,7 @@ def _add_to_compilation_queue(python_class: type | PythonSupplier) -> None: def _process_compilation_queue() -> None: global _compilation_queue + register_score_python_java_type_mappings() while len(_compilation_queue) > 0: python_class = _compilation_queue.pop(0) @@ -279,6 +323,7 @@ def _generate_constraint_provider_class(original_function: Callable[['_Constrain wrapped_constraint_provider: Callable[['_ConstraintFactory'], list['_Constraint']]) -> JClass: ensure_init() + register_score_python_java_type_mappings() from ai.timefold.solver.python import PythonWrapperGenerator # noqa from ai.timefold.solver.core.api.score.stream import ConstraintProvider class_identifier = _get_class_identifier_for_object(original_function) diff --git a/timefold-solver-python-core/src/main/python/score/__init__.py b/timefold-solver-python-core/src/main/python/score/__init__.py index faf26251..2e6f48e1 100644 --- a/timefold-solver-python-core/src/main/python/score/__init__.py +++ b/timefold-solver-python-core/src/main/python/score/__init__.py @@ -24,169 +24,6 @@ from ._group_by import * from ._incremental_score_calculator import * from ._joiners import * +from ._score import * from ._score_analysis import * from ._score_director import * - -from typing import TYPE_CHECKING as _TYPE_CHECKING - -if _TYPE_CHECKING: - class Score: - """ - A Score is result of the score function (AKA fitness function) on a single possible solution. - Implementations must be immutable. - - Attributes - ---------- - init_score : int - The init score is the negative of the number of uninitialized genuine planning variables. - If it's 0 (which it usually is), - the `planning_solution` is fully initialized and the score's str does not mention it. - For comparisons, it's even more important than the hard score: - if you don't want this behaviour, read about overconstrained planning in the reference manual. - - is_feasible : bool - A `planning_solution` is feasible if it has no broken hard constraints and `is_solution_initialized` is - true. `SimpleScore` are always feasible, if their `init_score` is 0. - - is_solution_initialized : bool - Checks if the `planning_solution` of this score was fully initialized when it was calculated. - True if `init_score` is 0. - - See Also - -------- - HardSoftScore - """ - init_score: int - is_feasible: bool - is_solution_initialized: bool - ... - - class SimpleScore(Score): - """ - This Score is based on one level of `int` constraints. - This class is immutable. - - Attributes - ---------- - score : int - The total of the broken negative constraints and fulfilled positive constraints. - Their weight is included in the total. - The score is usually a negative number because most use cases only have negative constraints. - """ - ZERO: 'SimpleScore' = None - ONE: 'SimpleScore' = None - score: int - - @staticmethod - def of(score: int, /) -> 'SimpleScore': - ... - - class HardSoftScore(Score): - """ - This Score is based on two levels of int constraints: hard and soft. - Hard constraints have priority over soft constraints. - Hard constraints determine feasibility. - - This class is immutable. - - Attributes - ---------- - hard_score : int - The total of the broken negative hard constraints and fulfilled positive hard constraints. - Their weight is included in the total. - The hard score is usually a negative number because most use cases only have negative constraints. - - soft_score : int - The total of the broken negative soft constraints and fulfilled positive soft constraints. - Their weight is included in the total. - The soft score is usually a negative number because most use cases only have negative constraints. - - In a normal score comparison, the soft score is irrelevant if the two scores don't have the same hard score. - """ - ZERO: 'HardSoftScore' = None - ONE_HARD: 'HardSoftScore' = None - ONE_SOFT: 'HardSoftScore' = None - hard_score: int - soft_score: int - - @staticmethod - def of(hard_score: int, soft_score: int, /) -> 'HardSoftScore': - ... - - - class HardMediumSoftScore(Score): - """ - This Score is based on three levels of int constraints: hard, medium and soft. - Hard constraints have priority over medium constraints. - Medium constraints have priority over soft constraints. - Hard constraints determine feasibility. - - This class is immutable. - - Attributes - ---------- - hard_score : int - The total of the broken negative hard constraints and fulfilled positive hard constraints. - Their weight is included in the total. - The hard score is usually a negative number because most use cases only have negative constraints. - - medium_score : int - The total of the broken negative medium constraints and fulfilled positive medium constraints. - Their weight is included in the total. - The medium score is usually a negative number because most use cases only have negative constraints. - - In a normal score comparison, - the medium score is irrelevant if the two scores don't have the same hard score. - - soft_score : int - The total of the broken negative soft constraints and fulfilled positive soft constraints. - Their weight is included in the total. - The soft score is usually a negative number because most use cases only have negative constraints. - - In a normal score comparison, - the soft score is irrelevant if the two scores don't have the same hard and medium score. - """ - ZERO: 'HardMediumSoftScore' = None - ONE_HARD: 'HardMediumSoftScore' = None - ONE_MEDIUM: 'HardMediumSoftScore' = None - ONE_SOFT: 'HardMediumSoftScore' = None - hard_score: int - medium_score: int - soft_score: int - - @staticmethod - def of(self, hard_score: int, medium_score: int, soft_score: int, /) -> 'HardMediumSoftScore': - ... - - class BendableScore(Score): - """ - This Score is based on n levels of int constraints. - The number of levels is bendable at configuration time. - - This class is immutable. - - Attributes - ---------- - hard_scores : list[int] - A list of hard scores, with earlier hard scores having higher priority than later ones. - - soft_scores : list[int] - A list of soft scores, with earlier soft scores having higher priority than later ones - """ - hard_scores: list[int] - soft_scores: list[int] - - @staticmethod - def of(hard_scores: list[int], soft_scores: list[int], /) -> 'BendableScore': - ... - - -def __getattr__(name): - from ._score import lookup_score_class - return lookup_score_class(name) - - -if not _TYPE_CHECKING: - exported = [name for name in globals().keys() if not name.startswith('_')] - exported += ['Score', 'SimpleScore', 'HardSoftScore', 'HardMediumSoftScore', 'BendableScore'] - __all__ = exported diff --git a/timefold-solver-python-core/src/main/python/score/_annotations.py b/timefold-solver-python-core/src/main/python/score/_annotations.py index 3e20425d..fb93ca06 100644 --- a/timefold-solver-python-core/src/main/python/score/_annotations.py +++ b/timefold-solver-python-core/src/main/python/score/_annotations.py @@ -83,9 +83,13 @@ def easy_score_calculator(easy_score_calculator_function: Callable[[Solution_], ensure_init() from _jpyinterpreter import translate_python_bytecode_to_java_bytecode, generate_proxy_class_for_translated_function from ai.timefold.solver.core.api.score.calculator import EasyScoreCalculator + + def wrapped_easy_score_calculator(solution): + return easy_score_calculator_function(solution)._to_java_score() + java_class = generate_proxy_class_for_translated_function(EasyScoreCalculator, translate_python_bytecode_to_java_bytecode( - easy_score_calculator_function, EasyScoreCalculator)) + wrapped_easy_score_calculator, EasyScoreCalculator)) return register_java_class(easy_score_calculator_function, java_class) diff --git a/timefold-solver-python-core/src/main/python/score/_constraint_builder.py b/timefold-solver-python-core/src/main/python/score/_constraint_builder.py index 724fe963..4795c161 100644 --- a/timefold-solver-python-core/src/main/python/score/_constraint_builder.py +++ b/timefold-solver-python-core/src/main/python/score/_constraint_builder.py @@ -1,5 +1,7 @@ import timefold.solver.score as score_api +from .._jpype_type_conversions import to_python_score from ._function_translator import function_cast +from ..score._score import Score from typing import TypeVar, Callable, Generic, Collection, TYPE_CHECKING, Type if TYPE_CHECKING: @@ -14,7 +16,7 @@ B = TypeVar('B') C = TypeVar('C') D = TypeVar('D') -ScoreType = TypeVar('ScoreType', bound='_JavaScore') +ScoreType = TypeVar('ScoreType', bound=Score) class Constraint: @@ -82,8 +84,12 @@ def justify_with(self, justification_function: Callable[[A, ScoreType], 'score_a this `UniConstraintBuilder`. """ from ai.timefold.solver.core.api.score import Score + + def wrapped(a, score): + return justification_function(a, to_python_score(score)) + return UniConstraintBuilder(self.delegate.justifyWith( - function_cast(justification_function, self.a_type, Score)), self.a_type) + function_cast(wrapped, self.a_type, Score)), self.a_type) def as_constraint(self, constraint_package_or_name: str, constraint_name: str = None) -> Constraint: """ @@ -171,8 +177,12 @@ def justify_with(self, justification_function: Callable[[A, B, ScoreType], this `BiConstraintBuilder`. """ from ai.timefold.solver.core.api.score import Score + + def wrapped(a, b, score): + return justification_function(a, b, to_python_score(score)) + return BiConstraintBuilder(self.delegate.justifyWith( - function_cast(justification_function, self.a_type, self.b_type, Score)), self.a_type, self.b_type) + function_cast(wrapped, self.a_type, self.b_type, Score)), self.a_type, self.b_type) def as_constraint(self, constraint_package_or_name: str, constraint_name: str = None) -> Constraint: """ @@ -264,8 +274,12 @@ def justify_with(self, justification_function: Callable[[A, B, C, ScoreType], this `TriConstraintBuilder`. """ from ai.timefold.solver.core.api.score import Score + + def wrapped(a, b, c, score): + return justification_function(a, b, c, to_python_score(score)) + return TriConstraintBuilder(self.delegate.justifyWith( - function_cast(justification_function, self.a_type, self.b_type, self.c_type, Score)), + function_cast(wrapped, self.a_type, self.b_type, self.c_type, Score)), self.a_type, self.b_type, self.c_type) def as_constraint(self, constraint_package_or_name: str, constraint_name: str = None) -> Constraint: @@ -360,8 +374,12 @@ def justify_with(self, justification_function: Callable[[A, B, C, D, ScoreType], this `QuadConstraintBuilder`. """ from ai.timefold.solver.core.api.score import Score + + def wrapped(a, b, c, d, score): + return justification_function(a, b, c, d, to_python_score(score)) + return QuadConstraintBuilder(self.delegate.justifyWith( - function_cast(justification_function, self.a_type, self.b_type, self.c_type, self.d_type, Score)), + function_cast(wrapped, self.a_type, self.b_type, self.c_type, self.d_type, Score)), self.a_type, self.b_type, self.c_type, self.d_type) def as_constraint(self, constraint_package_or_name: str, constraint_name: str = None) -> Constraint: diff --git a/timefold-solver-python-core/src/main/python/score/_incremental_score_calculator.py b/timefold-solver-python-core/src/main/python/score/_incremental_score_calculator.py index dee5df1a..9168763e 100644 --- a/timefold-solver-python-core/src/main/python/score/_incremental_score_calculator.py +++ b/timefold-solver-python-core/src/main/python/score/_incremental_score_calculator.py @@ -172,7 +172,7 @@ def beforeVariableChanged(self, entity, variable_name) -> None: IncrementalScoreCalculator.beforeVariableChanged = beforeVariableChanged def calculateScore(self): - return type(self).calculate_score(self) + return type(self).calculate_score(self)._to_java_score() IncrementalScoreCalculator.calculateScore = calculateScore diff --git a/timefold-solver-python-core/src/main/python/score/_score.py b/timefold-solver-python-core/src/main/python/score/_score.py index 3b57c343..4e81111c 100644 --- a/timefold-solver-python-core/src/main/python/score/_score.py +++ b/timefold-solver-python-core/src/main/python/score/_score.py @@ -1,122 +1,259 @@ -from .._timefold_java_interop import ensure_init -from jpype import JImplementationFor -import jpype.imports # noqa +from abc import ABC, abstractmethod +from typing import ClassVar +from dataclasses import dataclass, field +from jpype import JArray, JInt +from .._timefold_java_interop import _java_score_mapping_dict -@JImplementationFor('ai.timefold.solver.core.api.score.buildin.simple.SimpleScore') -class _SimpleScoreImpl: +@dataclass(unsafe_hash=True) +class Score(ABC): + """ + A Score is result of the score function (AKA fitness function) on a single possible solution. + Implementations must be immutable. + + Attributes + ---------- + init_score : int + The init score is the negative of the number of uninitialized genuine planning variables. + If it's 0 (which it usually is), + the `planning_solution` is fully initialized and the score's str does not mention it. + For comparisons, it's even more important than the hard score: + if you don't want this behaviour, read about overconstrained planning in the reference manual. + + is_feasible : bool + A `planning_solution` is feasible if it has no broken hard constraints and `is_solution_initialized` is + true. `SimpleScore` are always feasible, if their `init_score` is 0. + + is_solution_initialized : bool + Checks if the `planning_solution` of this score was fully initialized when it was calculated. + True if `init_score` is 0. + + See Also + -------- + HardSoftScore + """ + init_score: int = field(default=0, kw_only=True, compare=True) + @property - def init_score(self) -> int: - return self.initScore() + @abstractmethod + def is_feasible(self) -> bool: + ... + + @abstractmethod + def _to_java_score(self) -> object: + ... @property def is_solution_initialized(self) -> bool: - return self.isSolutionInitialized() + return self.init_score == 0 + + +@dataclass(unsafe_hash=True, order=True) +class SimpleScore(Score): + """ + This Score is based on one level of `int` constraints. + This class is immutable. + + Attributes + ---------- + score : int + The total of the broken negative constraints and fulfilled positive constraints. + Their weight is included in the total. + The score is usually a negative number because most use cases only have negative constraints. + """ + ZERO: ClassVar['SimpleScore'] + ONE: ClassVar['SimpleScore'] + + score: int = field(compare=True) @property def is_feasible(self) -> bool: - return self.isFeasible() + return self.is_solution_initialized - @property - def score(self): - return self.toLevelNumbers()[0] # noqa + @staticmethod + def of(score: int) -> 'SimpleScore': + return SimpleScore(score, init_score=0) + def _to_java_score(self): + if self.init_score < 0: + return _java_score_mapping_dict['SimpleScore'].ofUninitialized(self.init_score, self.score) + else: + return _java_score_mapping_dict['SimpleScore'].of(self.score) -@JImplementationFor('ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore') -class _HardSoftScoreImpl: - @property - def init_score(self) -> int: - return self.initScore() + def __str__(self): + return (f'{self.score}' if self.is_solution_initialized else + f'{self.init_score}init/{self.score}') - @property - def is_solution_initialized(self) -> bool: - return self.isSolutionInitialized() + +SimpleScore.ZERO = SimpleScore.of(0) +SimpleScore.ONE = SimpleScore.of(1) + + +@dataclass(unsafe_hash=True, order=True) +class HardSoftScore(Score): + """ + This Score is based on two levels of int constraints: hard and soft. + Hard constraints have priority over soft constraints. + Hard constraints determine feasibility. + + This class is immutable. + + Attributes + ---------- + hard_score : int + The total of the broken negative hard constraints and fulfilled positive hard constraints. + Their weight is included in the total. + The hard score is usually a negative number because most use cases only have negative constraints. + + soft_score : int + The total of the broken negative soft constraints and fulfilled positive soft constraints. + Their weight is included in the total. + The soft score is usually a negative number because most use cases only have negative constraints. + + In a normal score comparison, the soft score is irrelevant if the two scores don't have the same hard score. + """ + ZERO: ClassVar['HardSoftScore'] + ONE_HARD: ClassVar['HardSoftScore'] + ONE_SOFT: ClassVar['HardSoftScore'] + + hard_score: int = field(compare=True) + soft_score: int = field(compare=True) @property def is_feasible(self) -> bool: - return self.isFeasible() + return self.is_solution_initialized and self.hard_score >= 0 - @property - def hard_score(self): - return self.hardScore() # noqa + @staticmethod + def of(hard_score: int, soft_score: int) -> 'HardSoftScore': + return HardSoftScore(hard_score, soft_score, init_score=0) - @property - def soft_score(self): - return self.softScore() # noqa + def _to_java_score(self): + if self.init_score < 0: + return _java_score_mapping_dict['HardSoftScore'].ofUninitialized(self.init_score, self.hard_score, self.soft_score) + else: + return _java_score_mapping_dict['HardSoftScore'].of(self.hard_score, self.soft_score) + def __str__(self): + return (f'{self.hard_score}hard/{self.soft_score}soft' if self.is_solution_initialized else + f'{self.init_score}init/{self.hard_score}hard/{self.soft_score}soft') -@JImplementationFor('ai.timefold.solver.core.api.score.buildin.hardmediumsoft.' - 'HardMediumSoftScore') -class _HardMediumSoftScoreImpl: - @property - def init_score(self) -> int: - return self.initScore() - @property - def is_solution_initialized(self) -> bool: - return self.isSolutionInitialized() +HardSoftScore.ZERO = HardSoftScore.of(0, 0) +HardSoftScore.ONE_HARD = HardSoftScore.of(1, 0) +HardSoftScore.ONE_SOFT = HardSoftScore.of(0, 1) + + +@dataclass(unsafe_hash=True, order=True) +class HardMediumSoftScore(Score): + """ + This Score is based on three levels of int constraints: hard, medium and soft. + Hard constraints have priority over medium constraints. + Medium constraints have priority over soft constraints. + Hard constraints determine feasibility. + + This class is immutable. + + Attributes + ---------- + hard_score : int + The total of the broken negative hard constraints and fulfilled positive hard constraints. + Their weight is included in the total. + The hard score is usually a negative number because most use cases only have negative constraints. + + medium_score : int + The total of the broken negative medium constraints and fulfilled positive medium constraints. + Their weight is included in the total. + The medium score is usually a negative number because most use cases only have negative constraints. + + In a normal score comparison, + the medium score is irrelevant if the two scores don't have the same hard score. + + soft_score : int + The total of the broken negative soft constraints and fulfilled positive soft constraints. + Their weight is included in the total. + The soft score is usually a negative number because most use cases only have negative constraints. + + In a normal score comparison, + the soft score is irrelevant if the two scores don't have the same hard and medium score. + """ + ZERO: ClassVar['HardMediumSoftScore'] + ONE_HARD: ClassVar['HardMediumSoftScore'] + ONE_MEDIUM: ClassVar['HardMediumSoftScore'] + ONE_SOFT: ClassVar['HardMediumSoftScore'] + + hard_score: int = field(compare=True) + medium_score: int = field(compare=True) + soft_score: int = field(compare=True) @property def is_feasible(self) -> bool: - return self.isFeasible() + return self.is_solution_initialized and self.hard_score >= 0 - @property - def hard_score(self): - return self.hardScore() # noqa + @staticmethod + def of(hard_score: int, medium_score: int, soft_score: int) -> 'HardMediumSoftScore': + return HardMediumSoftScore(hard_score, medium_score, soft_score, init_score=0) - @property - def medium_score(self): - return self.mediumScore() # noqa + def _to_java_score(self): + if self.init_score < 0: + return _java_score_mapping_dict['HardMediumSoftScore'].ofUninitialized(self.init_score, self.hard_score, + self.medium_score, self.soft_score) + else: + return _java_score_mapping_dict['HardMediumSoftScore'].of(self.hard_score, self.medium_score, self.soft_score) - @property - def soft_score(self): - return self.softScore() # noqa + def __str__(self): + return (f'{self.hard_score}hard/{self.medium_score}medium/{self.soft_score}soft' + if self.is_solution_initialized else + f'{self.init_score}init/{self.hard_score}hard/{self.medium_score}medium/{self.soft_score}soft') -@JImplementationFor('ai.timefold.solver.core.api.score.buildin.bendable.BendableScore') -class _BendableScoreImpl: - @property - def init_score(self) -> int: - return self.initScore() +HardMediumSoftScore.ZERO = HardMediumSoftScore.of(0, 0, 0) +HardMediumSoftScore.ONE_HARD = HardMediumSoftScore.of(1, 0, 0) +HardMediumSoftScore.ONE_MEDIUM = HardMediumSoftScore.of(0, 1, 0) +HardMediumSoftScore.ONE_SOFT = HardMediumSoftScore.of(0, 0, 1) - @property - def is_solution_initialized(self) -> bool: - return self.isSolutionInitialized() + +@dataclass(unsafe_hash=True, order=True) +class BendableScore(Score): + """ + This Score is based on n levels of int constraints. + The number of levels is bendable at configuration time. + + This class is immutable. + + Attributes + ---------- + hard_scores : tuple[int, ...] + A tuple of hard scores, with earlier hard scores having higher priority than later ones. + + soft_scores : tuple[int, ...] + A tuple of soft scores, with earlier soft scores having higher priority than later ones + """ + hard_scores: tuple[int, ...] = field(compare=True) + soft_scores: tuple[int, ...] = field(compare=True) @property def is_feasible(self) -> bool: - return self.isFeasible() + return self.is_solution_initialized and all(score >= 0 for score in self.hard_scores) - @property - def hard_scores(self): - return self.hardScores() # noqa + @staticmethod + def of(hard_scores: tuple[int, ...], soft_scores: tuple[int, ...]) -> 'BendableScore': + return BendableScore(hard_scores, soft_scores, init_score=0) - @property - def soft_scores(self): - return self.softScores() # noqa - - -def lookup_score_class(name: str): - ensure_init() - import jpype.imports - from ai.timefold.solver.core.api.score import Score - from ai.timefold.solver.core.api.score.buildin.simple import SimpleScore - from ai.timefold.solver.core.api.score.buildin.hardsoft import HardSoftScore - from ai.timefold.solver.core.api.score.buildin.hardmediumsoft import HardMediumSoftScore - from ai.timefold.solver.core.api.score.buildin.bendable import BendableScore - match name: - case 'Score': - return Score - case 'SimpleScore': - return SimpleScore - case 'HardSoftScore': - return HardSoftScore - case 'HardMediumSoftScore': - return HardMediumSoftScore - case 'BendableScore': - return BendableScore - case _: - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -__all__ = ['lookup_score_class'] + def _to_java_score(self): + IntArrayCls = JArray(JInt) + hard_scores = IntArrayCls(self.hard_scores) + soft_scores = IntArrayCls(self.soft_scores) + if self.init_score < 0: + return _java_score_mapping_dict['BendableScore'].ofUninitialized(self.init_score, hard_scores, soft_scores) + else: + return _java_score_mapping_dict['BendableScore'].of(hard_scores, soft_scores) + + def __str__(self): + return (f'{list(self.hard_scores)}hard/{list(self.soft_scores)}soft' if self.is_solution_initialized else + f'{self.init_score}init/{list(self.hard_scores)}hard/{list(self.soft_scores)}soft') + + +# Import score conversions here to register conversions (circular import) +from ._score_conversions import * + +__all__ = ['Score', 'SimpleScore', 'HardSoftScore', 'HardMediumSoftScore', 'BendableScore'] diff --git a/timefold-solver-python-core/src/main/python/score/_score_analysis.py b/timefold-solver-python-core/src/main/python/score/_score_analysis.py index 26bb62a9..06b1874e 100644 --- a/timefold-solver-python-core/src/main/python/score/_score_analysis.py +++ b/timefold-solver-python-core/src/main/python/score/_score_analysis.py @@ -1,4 +1,5 @@ from .._timefold_java_interop import get_class +from .._jpype_type_conversions import to_python_score from _jpyinterpreter import unwrap_python_like_object, add_java_interface from dataclasses import dataclass @@ -107,7 +108,7 @@ def __hash__(self) -> int: combined_hash ^= _safe_hash(self.justification) for item in self.indicted_objects: combined_hash ^= _safe_hash(item) - combined_hash ^= self.score.hashCode() + combined_hash ^= self.score.__hash__() return combined_hash @@ -134,7 +135,7 @@ def __hash__(self) -> int: if self.constraint_weight is not None: combined_hash ^= self.constraint_weight.hashCode() - combined_hash ^= self.score.hashCode() + combined_hash ^= self.score.__hash__() return combined_hash @@ -185,7 +186,7 @@ class DefaultConstraintJustification(ConstraintJustification): impact: Score_ def __hash__(self) -> int: - combined_hash = self.impact.hashCode() + combined_hash = self.impact.__hash__() for fact in self.facts: combined_hash ^= _safe_hash(fact) return combined_hash @@ -200,7 +201,7 @@ def _map_constraint_match_set(constraint_match_set: set['_JavaConstraintMatch']) justification=_unwrap_justification(constraint_match.getJustification()), indicted_objects=tuple([unwrap_python_like_object(indicted) for indicted in cast(list, constraint_match.getIndictedObjectList())]), - score=constraint_match.getScore() + score=to_python_score(constraint_match.getScore()) ) for constraint_match in constraint_match_set } @@ -213,7 +214,7 @@ def _unwrap_justification(justification: Any) -> ConstraintJustification: fact_list = justification.getFacts() return DefaultConstraintJustification(facts=tuple([unwrap_python_like_object(fact) for fact in cast(list, fact_list)]), - impact=justification.getImpact()) + impact=to_python_score(justification.getImpact())) else: return unwrap_python_like_object(justification) @@ -247,7 +248,7 @@ def __init__(self, delegate: '_JavaIndictment[Score_]'): @property def score(self) -> Score_: - return self._delegate.getScore() + return to_python_score(self._delegate.getScore()) @property def constraint_match_count(self) -> int: @@ -339,8 +340,8 @@ def constraint_match_total_map(self) -> dict[str, ConstraintMatchTotal]: constraint_name=e.getValue().getConstraintRef().constraintName()), constraint_match_count=e.getValue().getConstraintMatchCount(), constraint_match_set=_map_constraint_match_set(e.getValue().getConstraintMatchSet()), - constraint_weight=e.getValue().getConstraintWeight(), - score=e.getValue().getScore() + constraint_weight=to_python_score(e.getValue().getConstraintWeight()), + score=to_python_score(e.getValue().getScore()) ) for e in cast(set['_JavaMap.Entry[str, _JavaConstraintMatchTotal]'], self._delegate.getConstraintMatchTotalMap().entrySet()) @@ -355,7 +356,7 @@ def indictment_map(self) -> dict[Any, Indictment]: @property def score(self) -> 'Score': - return self._delegate.getScore() + return to_python_score(self._delegate.getScore()) @property def solution(self) -> Solution_: @@ -421,7 +422,7 @@ def constraint_ref(self) -> ConstraintRef: @property def score(self) -> Score_: - return self._delegate.score() + return to_python_score(self._delegate.score()) @property def justification(self) -> ConstraintJustification: @@ -467,7 +468,7 @@ def constraint_name(self) -> str: @property def weight(self) -> Optional[Score_]: - return self._delegate.weight() + return to_python_score(self._delegate.weight()) @property def matches(self) -> list[MatchAnalysis[Score_]]: @@ -476,7 +477,7 @@ def matches(self) -> list[MatchAnalysis[Score_]]: @property def score(self) -> Score_: - return self._delegate.score() + return to_python_score(self._delegate.score()) class ScoreAnalysis: @@ -521,7 +522,7 @@ def __init__(self, delegate: '_JavaScoreAnalysis'): @property def score(self) -> 'Score': - return self._delegate.score() + return to_python_score(self._delegate.score()) @property def constraint_map(self) -> dict[ConstraintRef, ConstraintAnalysis]: diff --git a/timefold-solver-python-core/src/main/python/score/_score_conversions.py b/timefold-solver-python-core/src/main/python/score/_score_conversions.py new file mode 100644 index 00000000..8be78820 --- /dev/null +++ b/timefold-solver-python-core/src/main/python/score/_score_conversions.py @@ -0,0 +1,22 @@ +from jpype import JConversion +from ._score import * + + +@JConversion('ai.timefold.solver.core.api.score.Score', exact=SimpleScore) +def _convert_simple_score(jcls, score: SimpleScore): + return score._to_java_score() + + +@JConversion('ai.timefold.solver.core.api.score.Score', exact=HardSoftScore) +def _convert_hard_soft_score(jcls, score: HardSoftScore): + return score._to_java_score() + + +@JConversion('ai.timefold.solver.core.api.score.Score', exact=HardMediumSoftScore) +def _convert_hard_medium_soft_score(jcls, score: HardMediumSoftScore): + return score._to_java_score() + + +@JConversion('ai.timefold.solver.core.api.score.Score', exact=BendableScore) +def _convert_bendable_score(jcls, score: BendableScore): + return score._to_java_score() diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/BendableScorePythonJavaTypeMappingTest.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/BendableScorePythonJavaTypeMappingTest.java new file mode 100644 index 00000000..96e77b88 --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/BendableScorePythonJavaTypeMappingTest.java @@ -0,0 +1,78 @@ +package ai.timefold.solver.python.score; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.bendable.BendableScore; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BendableScorePythonJavaTypeMappingTest { + BendableScorePythonJavaTypeMapping typeMapping; + + @BeforeEach + void setUp() throws NoSuchFieldException, ClassNotFoundException, NoSuchMethodException { + this.typeMapping = new BendableScorePythonJavaTypeMapping(PythonBendableScore.TYPE); + } + + @Test + void getPythonType() { + assertThat(typeMapping.getPythonType()).isEqualTo(PythonBendableScore.TYPE); + } + + @Test + void getJavaType() { + assertThat(typeMapping.getJavaType()).isEqualTo(BendableScore.class); + } + + @Test + void toPythonObject() { + var initializedScore = BendableScore.of(new int[] { 10, 20, 30 }, new int[] { 4, 5 }); + + var initializedPythonScore = (PythonBendableScore) typeMapping.toPythonObject(initializedScore); + + assertThat(initializedPythonScore.init_score).isEqualTo(PythonInteger.ZERO); + + assertThat(initializedPythonScore.hard_scores.size()).isEqualTo(3); + assertThat(initializedPythonScore.hard_scores.get(0)).isEqualTo(PythonInteger.valueOf(10)); + assertThat(initializedPythonScore.hard_scores.get(1)).isEqualTo(PythonInteger.valueOf(20)); + assertThat(initializedPythonScore.hard_scores.get(2)).isEqualTo(PythonInteger.valueOf(30)); + + assertThat(initializedPythonScore.soft_scores.size()).isEqualTo(2); + assertThat(initializedPythonScore.soft_scores.get(0)).isEqualTo(PythonInteger.valueOf(4)); + assertThat(initializedPythonScore.soft_scores.get(1)).isEqualTo(PythonInteger.valueOf(5)); + + var uninitializedScore = BendableScore.ofUninitialized(-300, new int[] { 10, 20, 30 }, new int[] { 4, 5 }); + var uninitializedPythonScore = (PythonBendableScore) typeMapping.toPythonObject(uninitializedScore); + + assertThat(uninitializedPythonScore.init_score).isEqualTo(PythonInteger.valueOf(-300)); + + assertThat(uninitializedPythonScore.hard_scores.size()).isEqualTo(3); + assertThat(uninitializedPythonScore.hard_scores.get(0)).isEqualTo(PythonInteger.valueOf(10)); + assertThat(uninitializedPythonScore.hard_scores.get(1)).isEqualTo(PythonInteger.valueOf(20)); + assertThat(uninitializedPythonScore.hard_scores.get(2)).isEqualTo(PythonInteger.valueOf(30)); + + assertThat(uninitializedPythonScore.soft_scores.size()).isEqualTo(2); + assertThat(uninitializedPythonScore.soft_scores.get(0)).isEqualTo(PythonInteger.valueOf(4)); + assertThat(uninitializedPythonScore.soft_scores.get(1)).isEqualTo(PythonInteger.valueOf(5)); + } + + @Test + void toJavaObject() { + var initializedScore = PythonBendableScore.of(new int[] { 10, 20, 30 }, new int[] { 4, 5 }); + + var initializedJavaScore = typeMapping.toJavaObject(initializedScore); + + assertThat(initializedJavaScore.initScore()).isEqualTo(0); + assertThat(initializedJavaScore.hardScores()).containsExactly(10, 20, 30); + assertThat(initializedJavaScore.softScores()).containsExactly(4, 5); + + var uninitializedScore = PythonBendableScore.ofUninitialized(-300, new int[] { 10, 20, 30 }, new int[] { 4, 5 }); + var uninitializedJavaScore = typeMapping.toJavaObject(uninitializedScore); + + assertThat(uninitializedJavaScore.initScore()).isEqualTo(-300); + assertThat(uninitializedJavaScore.hardScores()).containsExactly(10, 20, 30); + assertThat(uninitializedJavaScore.softScores()).containsExactly(4, 5); + } +} \ No newline at end of file diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/HardMediumSoftScorePythonJavaTypeMappingTest.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/HardMediumSoftScorePythonJavaTypeMappingTest.java new file mode 100644 index 00000000..6890cba6 --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/HardMediumSoftScorePythonJavaTypeMappingTest.java @@ -0,0 +1,68 @@ +package ai.timefold.solver.python.score; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HardMediumSoftScorePythonJavaTypeMappingTest { + HardMediumSoftScorePythonJavaTypeMapping typeMapping; + + @BeforeEach + void setUp() throws NoSuchFieldException, ClassNotFoundException, NoSuchMethodException { + this.typeMapping = new HardMediumSoftScorePythonJavaTypeMapping(PythonHardMediumSoftScore.TYPE); + } + + @Test + void getPythonType() { + assertThat(typeMapping.getPythonType()).isEqualTo(PythonHardMediumSoftScore.TYPE); + } + + @Test + void getJavaType() { + assertThat(typeMapping.getJavaType()).isEqualTo(HardMediumSoftScore.class); + } + + @Test + void toPythonObject() { + var initializedScore = HardMediumSoftScore.of(300, 20, 1); + + var initializedPythonScore = (PythonHardMediumSoftScore) typeMapping.toPythonObject(initializedScore); + + assertThat(initializedPythonScore.init_score).isEqualTo(PythonInteger.ZERO); + assertThat(initializedPythonScore.hard_score).isEqualTo(PythonInteger.valueOf(300)); + assertThat(initializedPythonScore.medium_score).isEqualTo(PythonInteger.valueOf(20)); + assertThat(initializedPythonScore.soft_score).isEqualTo(PythonInteger.valueOf(1)); + + var uninitializedScore = HardMediumSoftScore.ofUninitialized(-4000, 300, 20, 1); + var uninitializedPythonScore = (PythonHardMediumSoftScore) typeMapping.toPythonObject(uninitializedScore); + + assertThat(uninitializedPythonScore.init_score).isEqualTo(PythonInteger.valueOf(-4000)); + assertThat(uninitializedPythonScore.hard_score).isEqualTo(PythonInteger.valueOf(300)); + assertThat(uninitializedPythonScore.medium_score).isEqualTo(PythonInteger.valueOf(20)); + assertThat(uninitializedPythonScore.soft_score).isEqualTo(PythonInteger.valueOf(1)); + } + + @Test + void toJavaObject() { + var initializedScore = PythonHardMediumSoftScore.of(300, 20, 1); + + var initializedJavaScore = typeMapping.toJavaObject(initializedScore); + + assertThat(initializedJavaScore.initScore()).isEqualTo(0); + assertThat(initializedJavaScore.hardScore()).isEqualTo(300); + assertThat(initializedJavaScore.mediumScore()).isEqualTo(20); + assertThat(initializedJavaScore.softScore()).isEqualTo(1); + + var uninitializedScore = PythonHardMediumSoftScore.ofUninitialized(-4000, 300, 20, 1); + var uninitializedJavaScore = typeMapping.toJavaObject(uninitializedScore); + + assertThat(uninitializedJavaScore.initScore()).isEqualTo(-4000); + assertThat(uninitializedJavaScore.hardScore()).isEqualTo(300); + assertThat(uninitializedJavaScore.mediumScore()).isEqualTo(20); + assertThat(uninitializedJavaScore.softScore()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/HardSoftScorePythonJavaTypeMappingTest.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/HardSoftScorePythonJavaTypeMappingTest.java new file mode 100644 index 00000000..a9c7f27a --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/HardSoftScorePythonJavaTypeMappingTest.java @@ -0,0 +1,64 @@ +package ai.timefold.solver.python.score; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HardSoftScorePythonJavaTypeMappingTest { + HardSoftScorePythonJavaTypeMapping typeMapping; + + @BeforeEach + void setUp() throws NoSuchFieldException, ClassNotFoundException, NoSuchMethodException { + this.typeMapping = new HardSoftScorePythonJavaTypeMapping(PythonHardSoftScore.TYPE); + } + + @Test + void getPythonType() { + assertThat(typeMapping.getPythonType()).isEqualTo(PythonHardSoftScore.TYPE); + } + + @Test + void getJavaType() { + assertThat(typeMapping.getJavaType()).isEqualTo(HardSoftScore.class); + } + + @Test + void toPythonObject() { + var initializedScore = HardSoftScore.of(10, 2); + + var initializedPythonScore = (PythonHardSoftScore) typeMapping.toPythonObject(initializedScore); + + assertThat(initializedPythonScore.init_score).isEqualTo(PythonInteger.ZERO); + assertThat(initializedPythonScore.hard_score).isEqualTo(PythonInteger.valueOf(10)); + assertThat(initializedPythonScore.soft_score).isEqualTo(PythonInteger.valueOf(2)); + + var uninitializedScore = HardSoftScore.ofUninitialized(-300, 20, 1); + var uninitializedPythonScore = (PythonHardSoftScore) typeMapping.toPythonObject(uninitializedScore); + + assertThat(uninitializedPythonScore.init_score).isEqualTo(PythonInteger.valueOf(-300)); + assertThat(uninitializedPythonScore.hard_score).isEqualTo(PythonInteger.valueOf(20)); + assertThat(uninitializedPythonScore.soft_score).isEqualTo(PythonInteger.valueOf(1)); + } + + @Test + void toJavaObject() { + var initializedScore = PythonHardSoftScore.of(10, 2); + + var initializedJavaScore = typeMapping.toJavaObject(initializedScore); + + assertThat(initializedJavaScore.initScore()).isEqualTo(0); + assertThat(initializedJavaScore.hardScore()).isEqualTo(10); + assertThat(initializedJavaScore.softScore()).isEqualTo(2); + + var uninitializedScore = PythonHardSoftScore.ofUninitialized(-300, 20, 1); + var uninitializedJavaScore = typeMapping.toJavaObject(uninitializedScore); + + assertThat(uninitializedJavaScore.initScore()).isEqualTo(-300); + assertThat(uninitializedJavaScore.hardScore()).isEqualTo(20); + assertThat(uninitializedJavaScore.softScore()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonBendableScore.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonBendableScore.java new file mode 100644 index 00000000..7bc264e6 --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonBendableScore.java @@ -0,0 +1,44 @@ +package ai.timefold.solver.python.score; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import ai.timefold.jpyinterpreter.types.AbstractPythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.collections.PythonLikeTuple; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; + +public class PythonBendableScore extends AbstractPythonLikeObject { + public static final PythonLikeType TYPE = new PythonLikeType("BendableScore", PythonBendableScore.class); + public PythonInteger init_score; + public PythonLikeTuple hard_scores; + public PythonLikeTuple soft_scores; + + public PythonBendableScore() { + super(TYPE); + } + + public static PythonBendableScore of(int[] hardScores, int[] softScores) { + var out = new PythonBendableScore(); + out.init_score = PythonInteger.ZERO; + out.hard_scores = IntStream.of(hardScores) + .mapToObj(PythonInteger::valueOf) + .collect(Collectors.toCollection(PythonLikeTuple::new)); + out.soft_scores = IntStream.of(softScores) + .mapToObj(PythonInteger::valueOf) + .collect(Collectors.toCollection(PythonLikeTuple::new)); + return out; + } + + public static PythonBendableScore ofUninitialized(int initScore, int[] hardScores, int[] softScores) { + var out = new PythonBendableScore(); + out.init_score = PythonInteger.valueOf(initScore); + out.hard_scores = IntStream.of(hardScores) + .mapToObj(PythonInteger::valueOf) + .collect(Collectors.toCollection(PythonLikeTuple::new)); + out.soft_scores = IntStream.of(softScores) + .mapToObj(PythonInteger::valueOf) + .collect(Collectors.toCollection(PythonLikeTuple::new)); + return out; + } +} diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonHardMediumSoftScore.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonHardMediumSoftScore.java new file mode 100644 index 00000000..94960149 --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonHardMediumSoftScore.java @@ -0,0 +1,35 @@ +package ai.timefold.solver.python.score; + +import ai.timefold.jpyinterpreter.types.AbstractPythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; + +public class PythonHardMediumSoftScore extends AbstractPythonLikeObject { + public static final PythonLikeType TYPE = new PythonLikeType("HardMediumSoftScore", PythonHardMediumSoftScore.class); + public PythonInteger init_score; + public PythonInteger hard_score; + public PythonInteger medium_score; + public PythonInteger soft_score; + + public PythonHardMediumSoftScore() { + super(TYPE); + } + + public static PythonHardMediumSoftScore of(int hardScore, int mediumScore, int softScore) { + var out = new PythonHardMediumSoftScore(); + out.init_score = PythonInteger.ZERO; + out.hard_score = PythonInteger.valueOf(hardScore); + out.medium_score = PythonInteger.valueOf(mediumScore); + out.soft_score = PythonInteger.valueOf(softScore); + return out; + } + + public static PythonHardMediumSoftScore ofUninitialized(int initScore, int hardScore, int mediumScore, int softScore) { + var out = new PythonHardMediumSoftScore(); + out.init_score = PythonInteger.valueOf(initScore); + out.hard_score = PythonInteger.valueOf(hardScore); + out.medium_score = PythonInteger.valueOf(mediumScore); + out.soft_score = PythonInteger.valueOf(softScore); + return out; + } +} diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonHardSoftScore.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonHardSoftScore.java new file mode 100644 index 00000000..29623da7 --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonHardSoftScore.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.python.score; + +import ai.timefold.jpyinterpreter.types.AbstractPythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; + +public class PythonHardSoftScore extends AbstractPythonLikeObject { + public static final PythonLikeType TYPE = new PythonLikeType("HardSoftScore", PythonHardSoftScore.class); + public PythonInteger init_score; + public PythonInteger hard_score; + public PythonInteger soft_score; + + public PythonHardSoftScore() { + super(TYPE); + } + + public static PythonHardSoftScore of(int hardScore, int softScore) { + var out = new PythonHardSoftScore(); + out.init_score = PythonInteger.ZERO; + out.hard_score = PythonInteger.valueOf(hardScore); + out.soft_score = PythonInteger.valueOf(softScore); + return out; + } + + public static PythonHardSoftScore ofUninitialized(int initScore, int hardScore, int softScore) { + var out = new PythonHardSoftScore(); + out.init_score = PythonInteger.valueOf(initScore); + out.hard_score = PythonInteger.valueOf(hardScore); + out.soft_score = PythonInteger.valueOf(softScore); + return out; + } +} diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonSimpleScore.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonSimpleScore.java new file mode 100644 index 00000000..dd245f93 --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/PythonSimpleScore.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.python.score; + +import ai.timefold.jpyinterpreter.types.AbstractPythonLikeObject; +import ai.timefold.jpyinterpreter.types.PythonLikeType; +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; + +public class PythonSimpleScore extends AbstractPythonLikeObject { + public static final PythonLikeType TYPE = new PythonLikeType("SimpleScore", PythonSimpleScore.class); + public PythonInteger init_score; + public PythonInteger score; + + public PythonSimpleScore() { + super(TYPE); + } + + public static PythonSimpleScore of(int score) { + var out = new PythonSimpleScore(); + out.init_score = PythonInteger.ZERO; + out.score = PythonInteger.valueOf(score); + return out; + } + + public static PythonSimpleScore ofUninitialized(int initScore, int score) { + var out = new PythonSimpleScore(); + out.init_score = PythonInteger.valueOf(initScore); + out.score = PythonInteger.valueOf(score); + return out; + } +} diff --git a/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/SimpleScorePythonJavaTypeMappingTest.java b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/SimpleScorePythonJavaTypeMappingTest.java new file mode 100644 index 00000000..406bc9ea --- /dev/null +++ b/timefold-solver-python-core/src/test/java/ai/timefold/solver/python/score/SimpleScorePythonJavaTypeMappingTest.java @@ -0,0 +1,61 @@ +package ai.timefold.solver.python.score; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import ai.timefold.jpyinterpreter.types.numeric.PythonInteger; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SimpleScorePythonJavaTypeMappingTest { + SimpleScorePythonJavaTypeMapping typeMapping; + + @BeforeEach + void setUp() throws NoSuchFieldException, ClassNotFoundException, NoSuchMethodException { + this.typeMapping = new SimpleScorePythonJavaTypeMapping(PythonSimpleScore.TYPE); + } + + @Test + void getPythonType() { + assertThat(typeMapping.getPythonType()).isEqualTo(PythonSimpleScore.TYPE); + } + + @Test + void getJavaType() { + assertThat(typeMapping.getJavaType()).isEqualTo(SimpleScore.class); + } + + @Test + void toPythonObject() { + var initializedScore = SimpleScore.of(10); + + var initializedPythonScore = (PythonSimpleScore) typeMapping.toPythonObject(initializedScore); + + assertThat(initializedPythonScore.init_score).isEqualTo(PythonInteger.ZERO); + assertThat(initializedPythonScore.score).isEqualTo(PythonInteger.valueOf(10)); + + var uninitializedScore = SimpleScore.ofUninitialized(-5, 20); + var uninitializedPythonScore = (PythonSimpleScore) typeMapping.toPythonObject(uninitializedScore); + + assertThat(uninitializedPythonScore.init_score).isEqualTo(PythonInteger.valueOf(-5)); + assertThat(uninitializedPythonScore.score).isEqualTo(PythonInteger.valueOf(20)); + } + + @Test + void toJavaObject() { + var initializedScore = PythonSimpleScore.of(10); + + var initializedJavaScore = typeMapping.toJavaObject(initializedScore); + + assertThat(initializedJavaScore.initScore()).isEqualTo(0); + assertThat(initializedJavaScore.score()).isEqualTo(10); + + var uninitializedScore = PythonSimpleScore.ofUninitialized(-5, 20); + var uninitializedJavaScore = typeMapping.toJavaObject(uninitializedScore); + + assertThat(uninitializedJavaScore.initScore()).isEqualTo(-5); + assertThat(uninitializedJavaScore.score()).isEqualTo(20); + } +} \ No newline at end of file