diff --git a/Cargo.lock b/Cargo.lock index b9ce3803..646044fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1327,7 +1327,6 @@ dependencies = [ "num", "ocipkg", "proptest", - "proptest-derive", "prost", "rand", "rand_xoshiro", @@ -1487,17 +1486,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "proptest-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "prost" version = "0.12.6" diff --git a/Cargo.toml b/Cargo.toml index f3b796f9..6b31c917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ maplit = "1.0.2" num = "0.4.3" ocipkg = "0.3.9" proptest = "1.5.0" -proptest-derive = "0.5.0" prost = "0.12.6" prost-build = "0.12.6" pyo3 = { version = "0.21.2", features = ["anyhow"] } diff --git a/Taskfile.yml b/Taskfile.yml index eec352b3..c1e35bcf 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -3,6 +3,15 @@ version: "3" tasks: + # Documents + doc_rust: + cmds: + - cargo doc --no-deps -p ommx + + doc_rust_open: + cmds: + - cargo doc --no-deps --open -p ommx + # Protocol Buffers protogen: cmds: diff --git a/proto/ommx/v1/instance.proto b/proto/ommx/v1/instance.proto index 9066aaa2..5ca08b52 100644 --- a/proto/ommx/v1/instance.proto +++ b/proto/ommx/v1/instance.proto @@ -6,6 +6,11 @@ import "ommx/v1/constraint.proto"; import "ommx/v1/decision_variables.proto"; import "ommx/v1/function.proto"; +// A set of parameters for instantiating an optimization problem from a parametric instance +message Parameters { + map entries = 1; +} + message Instance { message Description { optional string name = 1; @@ -45,4 +50,7 @@ message Instance { // - This is a required field. Most mathematical modeling tools allow for an empty sense and default to minimization. Alternatively, some tools do not create such a field and represent maximization problems by negating the objective function. This project prefers explicit descriptions over implicit ones to avoid such ambiguity and to make it unnecessary for developers to look up the reference for the treatment of omitted cases. // Sense sense = 5; + + // Parameters used when instantiating this instance + optional Parameters parameters = 6; } diff --git a/proto/ommx/v1/parametric_instance.proto b/proto/ommx/v1/parametric_instance.proto new file mode 100644 index 00000000..0c3a0dd7 --- /dev/null +++ b/proto/ommx/v1/parametric_instance.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package ommx.v1; + +import "ommx/v1/constraint.proto"; +import "ommx/v1/decision_variables.proto"; +import "ommx/v1/function.proto"; +import "ommx/v1/instance.proto"; + +// Placeholder of a parameter in a parametrized optimization problem +message Parameter { + // ID for the parameter + // + // - IDs are not required to be sequential. + // - The ID must be unique within the instance including the decision variables. + uint64 id = 1; + + // Name of the parameter. e.g. `x` + optional string name = 2; + + // Subscripts of the parameter, same usage as DecisionVariable.subscripts + repeated int64 subscripts = 3; + + // Additional metadata for the parameter, same usage as DecisionVariable.parameters + map parameters = 4; + + // Human-readable description for the parameter + optional string description = 5; +} + +// Optimization problem including parameter, variables varying while solving the problem like penalty weights or dual variables. +// These parameters are not decision variables. +message ParametricInstance { + Instance.Description description = 1; + + // Decision variables used in this instance + repeated DecisionVariable decision_variables = 2; + + // Parameters of this instance + // + // - The ID must be unique within the instance including the decision variables. + repeated Parameter parameters = 3; + + // Objective function of the optimization problem. This may contain parameters in addition to the decision variables. + Function objective = 4; + + // Constraints of the optimization problem. This may contain parameters in addition to the decision variables. + repeated Constraint constraints = 5; + + // The sense of this problem, i.e. minimize the objective or maximize it. + Instance.Sense sense = 6; +} diff --git a/python/ommx/ommx/v1/instance_pb2.py b/python/ommx/ommx/v1/instance_pb2.py index 831ddc61..9b1df2d0 100644 --- a/python/ommx/ommx/v1/instance_pb2.py +++ b/python/ommx/ommx/v1/instance_pb2.py @@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\x16ommx/v1/instance.proto\x12\x07ommx.v1\x1a\x18ommx/v1/constraint.proto\x1a ommx/v1/decision_variables.proto\x1a\x16ommx/v1/function.proto"\xaa\x04\n\x08Instance\x12?\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1d.ommx.v1.Instance.DescriptionR\x0b\x64\x65scription\x12H\n\x12\x64\x65\x63ision_variables\x18\x02 \x03(\x0b\x32\x19.ommx.v1.DecisionVariableR\x11\x64\x65\x63isionVariables\x12/\n\tobjective\x18\x03 \x01(\x0b\x32\x11.ommx.v1.FunctionR\tobjective\x12\x35\n\x0b\x63onstraints\x18\x04 \x03(\x0b\x32\x13.ommx.v1.ConstraintR\x0b\x63onstraints\x12-\n\x05sense\x18\x05 \x01(\x0e\x32\x17.ommx.v1.Instance.SenseR\x05sense\x1a\xb3\x01\n\x0b\x44\x65scription\x12\x17\n\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01\x12%\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x01R\x0b\x64\x65scription\x88\x01\x01\x12\x18\n\x07\x61uthors\x18\x03 \x03(\tR\x07\x61uthors\x12"\n\ncreated_by\x18\x04 \x01(\tH\x02R\tcreatedBy\x88\x01\x01\x42\x07\n\x05_nameB\x0e\n\x0c_descriptionB\r\n\x0b_created_by"F\n\x05Sense\x12\x15\n\x11SENSE_UNSPECIFIED\x10\x00\x12\x12\n\x0eSENSE_MINIMIZE\x10\x01\x12\x12\n\x0eSENSE_MAXIMIZE\x10\x02\x42Y\n\x0b\x63om.ommx.v1B\rInstanceProtoP\x01\xa2\x02\x03OXX\xaa\x02\x07Ommx.V1\xca\x02\x07Ommx\\V1\xe2\x02\x13Ommx\\V1\\GPBMetadata\xea\x02\x08Ommx::V1b\x06proto3' + b'\n\x16ommx/v1/instance.proto\x12\x07ommx.v1\x1a\x18ommx/v1/constraint.proto\x1a ommx/v1/decision_variables.proto\x1a\x16ommx/v1/function.proto"\x84\x01\n\nParameters\x12:\n\x07\x65ntries\x18\x01 \x03(\x0b\x32 .ommx.v1.Parameters.EntriesEntryR\x07\x65ntries\x1a:\n\x0c\x45ntriesEntry\x12\x10\n\x03key\x18\x01 \x01(\x04R\x03key\x12\x14\n\x05value\x18\x02 \x01(\x01R\x05value:\x02\x38\x01"\xf3\x04\n\x08Instance\x12?\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1d.ommx.v1.Instance.DescriptionR\x0b\x64\x65scription\x12H\n\x12\x64\x65\x63ision_variables\x18\x02 \x03(\x0b\x32\x19.ommx.v1.DecisionVariableR\x11\x64\x65\x63isionVariables\x12/\n\tobjective\x18\x03 \x01(\x0b\x32\x11.ommx.v1.FunctionR\tobjective\x12\x35\n\x0b\x63onstraints\x18\x04 \x03(\x0b\x32\x13.ommx.v1.ConstraintR\x0b\x63onstraints\x12-\n\x05sense\x18\x05 \x01(\x0e\x32\x17.ommx.v1.Instance.SenseR\x05sense\x12\x38\n\nparameters\x18\x06 \x01(\x0b\x32\x13.ommx.v1.ParametersH\x00R\nparameters\x88\x01\x01\x1a\xb3\x01\n\x0b\x44\x65scription\x12\x17\n\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01\x12%\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x01R\x0b\x64\x65scription\x88\x01\x01\x12\x18\n\x07\x61uthors\x18\x03 \x03(\tR\x07\x61uthors\x12"\n\ncreated_by\x18\x04 \x01(\tH\x02R\tcreatedBy\x88\x01\x01\x42\x07\n\x05_nameB\x0e\n\x0c_descriptionB\r\n\x0b_created_by"F\n\x05Sense\x12\x15\n\x11SENSE_UNSPECIFIED\x10\x00\x12\x12\n\x0eSENSE_MINIMIZE\x10\x01\x12\x12\n\x0eSENSE_MAXIMIZE\x10\x02\x42\r\n\x0b_parametersBY\n\x0b\x63om.ommx.v1B\rInstanceProtoP\x01\xa2\x02\x03OXX\xaa\x02\x07Ommx.V1\xca\x02\x07Ommx\\V1\xe2\x02\x13Ommx\\V1\\GPBMetadata\xea\x02\x08Ommx::V1b\x06proto3' ) _globals = globals() @@ -30,10 +30,16 @@ _globals[ "DESCRIPTOR" ]._serialized_options = b"\n\013com.ommx.v1B\rInstanceProtoP\001\242\002\003OXX\252\002\007Ommx.V1\312\002\007Ommx\\V1\342\002\023Ommx\\V1\\GPBMetadata\352\002\010Ommx::V1" - _globals["_INSTANCE"]._serialized_start = 120 - _globals["_INSTANCE"]._serialized_end = 674 - _globals["_INSTANCE_DESCRIPTION"]._serialized_start = 423 - _globals["_INSTANCE_DESCRIPTION"]._serialized_end = 602 - _globals["_INSTANCE_SENSE"]._serialized_start = 604 - _globals["_INSTANCE_SENSE"]._serialized_end = 674 + _globals["_PARAMETERS_ENTRIESENTRY"]._loaded_options = None + _globals["_PARAMETERS_ENTRIESENTRY"]._serialized_options = b"8\001" + _globals["_PARAMETERS"]._serialized_start = 120 + _globals["_PARAMETERS"]._serialized_end = 252 + _globals["_PARAMETERS_ENTRIESENTRY"]._serialized_start = 194 + _globals["_PARAMETERS_ENTRIESENTRY"]._serialized_end = 252 + _globals["_INSTANCE"]._serialized_start = 255 + _globals["_INSTANCE"]._serialized_end = 882 + _globals["_INSTANCE_DESCRIPTION"]._serialized_start = 616 + _globals["_INSTANCE_DESCRIPTION"]._serialized_end = 795 + _globals["_INSTANCE_SENSE"]._serialized_start = 797 + _globals["_INSTANCE_SENSE"]._serialized_end = 867 # @@protoc_insertion_point(module_scope) diff --git a/python/ommx/ommx/v1/instance_pb2.pyi b/python/ommx/ommx/v1/instance_pb2.pyi index c8a21b55..f47a47ea 100644 --- a/python/ommx/ommx/v1/instance_pb2.pyi +++ b/python/ommx/ommx/v1/instance_pb2.pyi @@ -22,6 +22,46 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +@typing.final +class Parameters(google.protobuf.message.Message): + """A set of parameters for instantiating an optimization problem from a parametric instance""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class EntriesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.int + value: builtins.float + def __init__( + self, + *, + key: builtins.int = ..., + value: builtins.float = ..., + ) -> None: ... + def ClearField( + self, field_name: typing.Literal["key", b"key", "value", b"value"] + ) -> None: ... + + ENTRIES_FIELD_NUMBER: builtins.int + @property + def entries( + self, + ) -> google.protobuf.internal.containers.ScalarMap[ + builtins.int, builtins.float + ]: ... + def __init__( + self, + *, + entries: collections.abc.Mapping[builtins.int, builtins.float] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["entries", b"entries"]) -> None: ... + +global___Parameters = Parameters + @typing.final class Instance(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -134,6 +174,7 @@ class Instance(google.protobuf.message.Message): OBJECTIVE_FIELD_NUMBER: builtins.int CONSTRAINTS_FIELD_NUMBER: builtins.int SENSE_FIELD_NUMBER: builtins.int + PARAMETERS_FIELD_NUMBER: builtins.int sense: global___Instance.Sense.ValueType """The sense of this problem, i.e. minimize the objective or maximize it. @@ -164,6 +205,10 @@ class Instance(google.protobuf.message.Message): ]: """Constraints of the optimization problem""" + @property + def parameters(self) -> global___Parameters: + """Parameters used when instantiating this instance""" + def __init__( self, *, @@ -176,16 +221,26 @@ class Instance(google.protobuf.message.Message): constraints: collections.abc.Iterable[ommx.v1.constraint_pb2.Constraint] | None = ..., sense: global___Instance.Sense.ValueType = ..., + parameters: global___Parameters | None = ..., ) -> None: ... def HasField( self, field_name: typing.Literal[ - "description", b"description", "objective", b"objective" + "_parameters", + b"_parameters", + "description", + b"description", + "objective", + b"objective", + "parameters", + b"parameters", ], ) -> builtins.bool: ... def ClearField( self, field_name: typing.Literal[ + "_parameters", + b"_parameters", "constraints", b"constraints", "decision_variables", @@ -194,9 +249,14 @@ class Instance(google.protobuf.message.Message): b"description", "objective", b"objective", + "parameters", + b"parameters", "sense", b"sense", ], ) -> None: ... + def WhichOneof( + self, oneof_group: typing.Literal["_parameters", b"_parameters"] + ) -> typing.Literal["parameters"] | None: ... global___Instance = Instance diff --git a/python/ommx/ommx/v1/parametric_instance_pb2.py b/python/ommx/ommx/v1/parametric_instance_pb2.py new file mode 100644 index 00000000..485c5571 --- /dev/null +++ b/python/ommx/ommx/v1/parametric_instance_pb2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ommx/v1/parametric_instance.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" + +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from ommx.v1 import constraint_pb2 as ommx_dot_v1_dot_constraint__pb2 +from ommx.v1 import decision_variables_pb2 as ommx_dot_v1_dot_decision__variables__pb2 +from ommx.v1 import function_pb2 as ommx_dot_v1_dot_function__pb2 +from ommx.v1 import instance_pb2 as ommx_dot_v1_dot_instance__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n!ommx/v1/parametric_instance.proto\x12\x07ommx.v1\x1a\x18ommx/v1/constraint.proto\x1a ommx/v1/decision_variables.proto\x1a\x16ommx/v1/function.proto\x1a\x16ommx/v1/instance.proto"\x97\x02\n\tParameter\x12\x0e\n\x02id\x18\x01 \x01(\x04R\x02id\x12\x17\n\x04name\x18\x02 \x01(\tH\x00R\x04name\x88\x01\x01\x12\x1e\n\nsubscripts\x18\x03 \x03(\x03R\nsubscripts\x12\x42\n\nparameters\x18\x04 \x03(\x0b\x32".ommx.v1.Parameter.ParametersEntryR\nparameters\x12%\n\x0b\x64\x65scription\x18\x05 \x01(\tH\x01R\x0b\x64\x65scription\x88\x01\x01\x1a=\n\x0fParametersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x07\n\x05_nameB\x0e\n\x0c_description"\xea\x02\n\x12ParametricInstance\x12?\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1d.ommx.v1.Instance.DescriptionR\x0b\x64\x65scription\x12H\n\x12\x64\x65\x63ision_variables\x18\x02 \x03(\x0b\x32\x19.ommx.v1.DecisionVariableR\x11\x64\x65\x63isionVariables\x12\x32\n\nparameters\x18\x03 \x03(\x0b\x32\x12.ommx.v1.ParameterR\nparameters\x12/\n\tobjective\x18\x04 \x01(\x0b\x32\x11.ommx.v1.FunctionR\tobjective\x12\x35\n\x0b\x63onstraints\x18\x05 \x03(\x0b\x32\x13.ommx.v1.ConstraintR\x0b\x63onstraints\x12-\n\x05sense\x18\x06 \x01(\x0e\x32\x17.ommx.v1.Instance.SenseR\x05senseBc\n\x0b\x63om.ommx.v1B\x17ParametricInstanceProtoP\x01\xa2\x02\x03OXX\xaa\x02\x07Ommx.V1\xca\x02\x07Ommx\\V1\xe2\x02\x13Ommx\\V1\\GPBMetadata\xea\x02\x08Ommx::V1b\x06proto3' +) + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages( + DESCRIPTOR, "ommx.v1.parametric_instance_pb2", _globals +) +if not _descriptor._USE_C_DESCRIPTORS: + _globals["DESCRIPTOR"]._loaded_options = None + _globals[ + "DESCRIPTOR" + ]._serialized_options = b"\n\013com.ommx.v1B\027ParametricInstanceProtoP\001\242\002\003OXX\252\002\007Ommx.V1\312\002\007Ommx\\V1\342\002\023Ommx\\V1\\GPBMetadata\352\002\010Ommx::V1" + _globals["_PARAMETER_PARAMETERSENTRY"]._loaded_options = None + _globals["_PARAMETER_PARAMETERSENTRY"]._serialized_options = b"8\001" + _globals["_PARAMETER"]._serialized_start = 155 + _globals["_PARAMETER"]._serialized_end = 434 + _globals["_PARAMETER_PARAMETERSENTRY"]._serialized_start = 348 + _globals["_PARAMETER_PARAMETERSENTRY"]._serialized_end = 409 + _globals["_PARAMETRICINSTANCE"]._serialized_start = 437 + _globals["_PARAMETRICINSTANCE"]._serialized_end = 799 +# @@protoc_insertion_point(module_scope) diff --git a/python/ommx/ommx/v1/parametric_instance_pb2.pyi b/python/ommx/ommx/v1/parametric_instance_pb2.pyi new file mode 100644 index 00000000..0513df87 --- /dev/null +++ b/python/ommx/ommx/v1/parametric_instance_pb2.pyi @@ -0,0 +1,209 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import ommx.v1.constraint_pb2 +import ommx.v1.decision_variables_pb2 +import ommx.v1.function_pb2 +import ommx.v1.instance_pb2 +import typing + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing.final +class Parameter(google.protobuf.message.Message): + """Placeholder of a parameter in a parametrized optimization problem""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class ParametersEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField( + self, field_name: typing.Literal["key", b"key", "value", b"value"] + ) -> None: ... + + ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + SUBSCRIPTS_FIELD_NUMBER: builtins.int + PARAMETERS_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + id: builtins.int + """ID for the parameter + + - IDs are not required to be sequential. + - The ID must be unique within the instance including the decision variables. + """ + name: builtins.str + """Name of the parameter. e.g. `x`""" + description: builtins.str + """Human-readable description for the parameter""" + @property + def subscripts( + self, + ) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: + """Subscripts of the parameter, same usage as DecisionVariable.subscripts""" + + @property + def parameters( + self, + ) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Additional metadata for the parameter, same usage as DecisionVariable.parameters""" + + def __init__( + self, + *, + id: builtins.int = ..., + name: builtins.str | None = ..., + subscripts: collections.abc.Iterable[builtins.int] | None = ..., + parameters: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + description: builtins.str | None = ..., + ) -> None: ... + def HasField( + self, + field_name: typing.Literal[ + "_description", + b"_description", + "_name", + b"_name", + "description", + b"description", + "name", + b"name", + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing.Literal[ + "_description", + b"_description", + "_name", + b"_name", + "description", + b"description", + "id", + b"id", + "name", + b"name", + "parameters", + b"parameters", + "subscripts", + b"subscripts", + ], + ) -> None: ... + @typing.overload + def WhichOneof( + self, oneof_group: typing.Literal["_description", b"_description"] + ) -> typing.Literal["description"] | None: ... + @typing.overload + def WhichOneof( + self, oneof_group: typing.Literal["_name", b"_name"] + ) -> typing.Literal["name"] | None: ... + +global___Parameter = Parameter + +@typing.final +class ParametricInstance(google.protobuf.message.Message): + """Optimization problem including parameter, variables varying while solving the problem like penalty weights or dual variables. + These parameters are not decision variables. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + DECISION_VARIABLES_FIELD_NUMBER: builtins.int + PARAMETERS_FIELD_NUMBER: builtins.int + OBJECTIVE_FIELD_NUMBER: builtins.int + CONSTRAINTS_FIELD_NUMBER: builtins.int + SENSE_FIELD_NUMBER: builtins.int + sense: ommx.v1.instance_pb2.Instance.Sense.ValueType + """The sense of this problem, i.e. minimize the objective or maximize it.""" + @property + def description(self) -> ommx.v1.instance_pb2.Instance.Description: ... + @property + def decision_variables( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + ommx.v1.decision_variables_pb2.DecisionVariable + ]: + """Decision variables used in this instance""" + + @property + def parameters( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + global___Parameter + ]: + """Parameters of this instance + + - The ID must be unique within the instance including the decision variables. + """ + + @property + def objective(self) -> ommx.v1.function_pb2.Function: + """Objective function of the optimization problem. This may contain parameters in addition to the decision variables.""" + + @property + def constraints( + self, + ) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[ + ommx.v1.constraint_pb2.Constraint + ]: + """Constraints of the optimization problem. This may contain parameters in addition to the decision variables.""" + + def __init__( + self, + *, + description: ommx.v1.instance_pb2.Instance.Description | None = ..., + decision_variables: collections.abc.Iterable[ + ommx.v1.decision_variables_pb2.DecisionVariable + ] + | None = ..., + parameters: collections.abc.Iterable[global___Parameter] | None = ..., + objective: ommx.v1.function_pb2.Function | None = ..., + constraints: collections.abc.Iterable[ommx.v1.constraint_pb2.Constraint] + | None = ..., + sense: ommx.v1.instance_pb2.Instance.Sense.ValueType = ..., + ) -> None: ... + def HasField( + self, + field_name: typing.Literal[ + "description", b"description", "objective", b"objective" + ], + ) -> builtins.bool: ... + def ClearField( + self, + field_name: typing.Literal[ + "constraints", + b"constraints", + "decision_variables", + b"decision_variables", + "description", + b"description", + "objective", + b"objective", + "parameters", + b"parameters", + "sense", + b"sense", + ], + ) -> None: ... + +global___ParametricInstance = ParametricInstance diff --git a/rust/ommx/Cargo.toml b/rust/ommx/Cargo.toml index ff1d9976..4ecd88a7 100644 --- a/rust/ommx/Cargo.toml +++ b/rust/ommx/Cargo.toml @@ -31,7 +31,6 @@ maplit.workspace = true num.workspace = true ocipkg.workspace = true proptest.workspace = true -proptest-derive.workspace = true prost.workspace = true rand.workspace = true rand_xoshiro.workspace = true diff --git a/rust/ommx/src/convert.rs b/rust/ommx/src/convert.rs index 57fa6f7f..2f1eb97b 100644 --- a/rust/ommx/src/convert.rs +++ b/rust/ommx/src/convert.rs @@ -63,6 +63,13 @@ macro_rules! impl_neg_by_mul { self * -1.0 } } + + impl ::std::ops::Neg for &$ty { + type Output = $ty; + fn neg(self) -> Self::Output { + self.clone() * -1.0 + } + } }; } @@ -147,6 +154,7 @@ mod format; mod function; mod instance; mod linear; +mod parametric_instance; mod polynomial; mod quadratic; mod state; diff --git a/rust/ommx/src/convert/constraint.rs b/rust/ommx/src/convert/constraint.rs index e1d1bd19..1c719ec0 100644 --- a/rust/ommx/src/convert/constraint.rs +++ b/rust/ommx/src/convert/constraint.rs @@ -1,5 +1,6 @@ use crate::v1::{Constraint, Equality, Function}; use anyhow::{Context, Result}; +use approx::AbsDiffEq; use proptest::prelude::*; impl Constraint { @@ -10,6 +11,25 @@ impl Constraint { } } +impl AbsDiffEq for Constraint { + type Epsilon = f64; + + fn default_epsilon() -> Self::Epsilon { + f64::EPSILON + } + + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + if self.equality != other.equality { + return false; + } + if let (Some(f), Some(g)) = (&self.function, &other.function) { + f.abs_diff_eq(g, epsilon) + } else { + false + } + } +} + impl Arbitrary for Constraint { type Parameters = ::Parameters; type Strategy = BoxedStrategy; diff --git a/rust/ommx/src/convert/decision_variable.rs b/rust/ommx/src/convert/decision_variable.rs index cc9c9ee5..4d033d31 100644 --- a/rust/ommx/src/convert/decision_variable.rs +++ b/rust/ommx/src/convert/decision_variable.rs @@ -54,13 +54,21 @@ impl Arbitrary for DecisionVariable { type Strategy = BoxedStrategy; fn arbitrary_with(max_id: Self::Parameters) -> Self::Strategy { + let subscripts = prop_oneof![ + Just(Vec::::new()), + proptest::collection::vec(-(max_id as i64)..=(max_id as i64), 1..=3), + ]; + let parameters = prop_oneof![ + Just(HashMap::::new()), + proptest::collection::hash_map(String::arbitrary(), String::arbitrary(), 1..=3), + ]; ( 0..=max_id, Option::::arbitrary(), Option::::arbitrary(), Kind::arbitrary(), - Vec::::arbitrary(), - HashMap::::arbitrary(), + subscripts, + parameters, Option::::arbitrary(), ) .prop_map( diff --git a/rust/ommx/src/convert/instance.rs b/rust/ommx/src/convert/instance.rs index 47a59be0..5f104b3d 100644 --- a/rust/ommx/src/convert/instance.rs +++ b/rust/ommx/src/convert/instance.rs @@ -6,9 +6,10 @@ use crate::{ }, }; use anyhow::{bail, Context, Result}; +use approx::AbsDiffEq; use proptest::prelude::*; use rand::SeedableRng; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use super::{constraint::arbitrary_constraints, decision_variable::arbitrary_decision_variables}; @@ -116,6 +117,7 @@ impl Arbitrary for Instance { decision_variables, description, sense: sense as i32, + ..Default::default() } }, ) @@ -134,6 +136,86 @@ impl Arbitrary for Sense { } } +impl Arbitrary for Description { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_parameter: ()) -> Self::Strategy { + ( + Option::::arbitrary(), + Option::::arbitrary(), + prop_oneof![Just(Vec::new()), proptest::collection::vec(".*", 1..3)], + Option::::arbitrary(), + ) + .prop_map(|(name, description, authors, created_by)| Description { + name, + description, + authors, + created_by, + }) + .boxed() + } +} + +/// Compare two instances as mathematical programming problems. This does not compare the metadata. +/// +/// - This regards `min f` and `max -f` as the same problem. +/// - This cannot compare scaled constraints. For example, `2x + 3y <= 4` and `4x + 6y <= 8` are mathematically same, +/// but this regarded them as different problems. +/// +impl AbsDiffEq for Instance { + type Epsilon = f64; + + fn default_epsilon() -> Self::Epsilon { + f64::default_epsilon() + } + + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + let (Some(f), Some(g)) = (&self.objective, &other.objective) else { + // Return false if one of instance is invalid + return false; + }; + match (self.sense.try_into(), other.sense.try_into()) { + (Ok(Sense::Minimize), Ok(Sense::Minimize)) + | (Ok(Sense::Maximize), Ok(Sense::Maximize)) => { + if !f.abs_diff_eq(g, epsilon) { + return false; + } + } + (Ok(Sense::Minimize), Ok(Sense::Maximize)) + | (Ok(Sense::Maximize), Ok(Sense::Minimize)) => { + if !f.abs_diff_eq(&-g, epsilon) { + return false; + } + } + _ => return false, + } + + if self.constraints.len() != other.constraints.len() { + return false; + } + // The constraints may not ordered in the same way + let lhs = self + .constraints + .iter() + .map(|c| (c.id, (c.equality, c.function()))) + .collect::>(); + for c in &other.constraints { + if let (Some((eq, Ok(f))), Ok(g)) = (lhs.get(&c.id), c.function()) { + if *eq != c.equality { + return false; + } + if !(*f).abs_diff_eq(g, epsilon) { + return false; + } + } else { + return false; + } + } + true + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/ommx/src/convert/parametric_instance.rs b/rust/ommx/src/convert/parametric_instance.rs new file mode 100644 index 00000000..f662a1dc --- /dev/null +++ b/rust/ommx/src/convert/parametric_instance.rs @@ -0,0 +1,125 @@ +use crate::{ + v1::{Function, Instance, Parameters, ParametricInstance, State}, + Evaluate, +}; +use anyhow::{bail, Context, Result}; +use std::collections::BTreeSet; + +impl From for ParametricInstance { + fn from( + Instance { + description, + objective, + constraints, + decision_variables, + sense, + parameters: _, // Drop previous parameters + }: Instance, + ) -> Self { + Self { + description, + objective, + constraints, + decision_variables, + sense, + parameters: Default::default(), + } + } +} + +impl From for Parameters { + fn from(State { entries }: State) -> Self { + Self { entries } + } +} + +impl From for State { + fn from(Parameters { entries }: Parameters) -> Self { + Self { entries } + } +} + +impl ParametricInstance { + /// Create a new [Instance] with the given parameters. + pub fn with_parameters(mut self, parameters: Parameters) -> Result { + let required_ids: BTreeSet = self.parameters.iter().map(|p| p.id).collect(); + let given_ids: BTreeSet = parameters.entries.keys().cloned().collect(); + if !required_ids.is_subset(&given_ids) { + for ids in required_ids.difference(&given_ids) { + let parameter = self.parameters.iter().find(|p| p.id == *ids).unwrap(); + log::error!("Missing parameter: {:?}", parameter); + } + bail!( + "Missing parameters: Required IDs {:?}, got {:?}", + required_ids, + given_ids + ); + } + + let state = State::from(parameters.clone()); + self.objective + .as_mut() + .context("Objective function of ParametricInstance is empty")? + .partial_evaluate(&state)?; + for constraint in self.constraints.iter_mut() { + constraint.partial_evaluate(&state)?; + } + + Ok(Instance { + description: self.description, + objective: self.objective, + constraints: self.constraints, + decision_variables: self.decision_variables, + sense: self.sense, + parameters: Some(parameters), + }) + } + + pub fn objective(&self) -> Result<&Function> { + self.objective + .as_ref() + .context("Objective function of ParametricInstance is empty") + } + + /// Used decision variable and parameter IDs in the objective and constraints. + pub fn used_ids(&self) -> Result> { + let mut used_ids = self.objective()?.used_decision_variable_ids(); + for c in &self.constraints { + used_ids.extend(c.function()?.used_decision_variable_ids()); + } + Ok(used_ids) + } + + /// Defined decision variable IDs. These IDs may not be used in the objective and constraints. + pub fn defined_decision_variable_ids(&self) -> BTreeSet { + self.decision_variables + .iter() + .map(|dv| dv.id) + .collect::>() + } + + /// Defined parameter IDs. These IDs may not be used in the objective and constraints. + pub fn defined_parameter_ids(&self) -> BTreeSet { + self.parameters.iter().map(|p| p.id).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::abs_diff_eq; + use proptest::prelude::*; + + proptest! { + #[test] + fn test_parametric_instance_conversion(instance in Instance::arbitrary()) { + let parametric_instance: ParametricInstance = instance.clone().into(); + let converted_instance: Instance = parametric_instance.with_parameters(Parameters::default()).unwrap(); + prop_assert_eq!(&converted_instance.parameters, &Some(Parameters::default())); + prop_assert!( + abs_diff_eq!(instance, converted_instance, epsilon = 1e-10), + "\nLeft : {:?}\nRight: {:?}", instance, converted_instance + ); + } + } +} diff --git a/rust/ommx/src/mps/convert.rs b/rust/ommx/src/mps/convert.rs index b03aef3c..09ad810d 100644 --- a/rust/ommx/src/mps/convert.rs +++ b/rust/ommx/src/mps/convert.rs @@ -16,6 +16,7 @@ pub fn convert(mps: Mps) -> Result { objective: Some(objective), constraints, sense: convert_sense(mps.obj_sense), + parameters: None, }) } diff --git a/rust/ommx/src/ommx.v1.rs b/rust/ommx/src/ommx.v1.rs index 629747ba..5d43cf4f 100644 --- a/rust/ommx/src/ommx.v1.rs +++ b/rust/ommx/src/ommx.v1.rs @@ -289,6 +289,14 @@ pub mod decision_variable { } } } +/// A set of parameters for instantiating an optimization problem from a parametric instance +#[non_exhaustive] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Parameters { + #[prost(map = "uint64, double", tag = "1")] + pub entries: ::std::collections::HashMap, +} #[non_exhaustive] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -313,11 +321,13 @@ pub struct Instance { /// #[prost(enumeration = "instance::Sense", tag = "5")] pub sense: i32, + /// Parameters used when instantiating this instance + #[prost(message, optional, tag = "6")] + pub parameters: ::core::option::Option, } /// Nested message and enum types in `Instance`. pub mod instance { #[non_exhaustive] - #[derive(::proptest_derive::Arbitrary)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Description { @@ -363,6 +373,57 @@ pub mod instance { } } } +/// Placeholder of a parameter in a parametrized optimization problem +#[non_exhaustive] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Parameter { + /// ID for the parameter + /// + /// - IDs are not required to be sequential. + /// - The ID must be unique within the instance including the decision variables. + #[prost(uint64, tag = "1")] + pub id: u64, + /// Name of the parameter. e.g. `x` + #[prost(string, optional, tag = "2")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + /// Subscripts of the parameter, same usage as DecisionVariable.subscripts + #[prost(int64, repeated, tag = "3")] + pub subscripts: ::prost::alloc::vec::Vec, + /// Additional metadata for the parameter, same usage as DecisionVariable.parameters + #[prost(map = "string, string", tag = "4")] + pub parameters: + ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, + /// Human-readable description for the parameter + #[prost(string, optional, tag = "5")] + pub description: ::core::option::Option<::prost::alloc::string::String>, +} +/// Optimization problem including parameter, variables varying while solving the problem like penalty weights or dual variables. +/// These parameters are not decision variables. +#[non_exhaustive] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ParametricInstance { + #[prost(message, optional, tag = "1")] + pub description: ::core::option::Option, + /// Decision variables used in this instance + #[prost(message, repeated, tag = "2")] + pub decision_variables: ::prost::alloc::vec::Vec, + /// Parameters of this instance + /// + /// - The ID must be unique within the instance including the decision variables. + #[prost(message, repeated, tag = "3")] + pub parameters: ::prost::alloc::vec::Vec, + /// Objective function of the optimization problem. This may contain parameters in addition to the decision variables. + #[prost(message, optional, tag = "4")] + pub objective: ::core::option::Option, + /// Constraints of the optimization problem. This may contain parameters in addition to the decision variables. + #[prost(message, repeated, tag = "5")] + pub constraints: ::prost::alloc::vec::Vec, + /// The sense of this problem, i.e. minimize the objective or maximize it. + #[prost(enumeration = "instance::Sense", tag = "6")] + pub sense: i32, +} /// A set of values of decision variables, without any evaluation, even the /// feasiblity of the solution. #[non_exhaustive] diff --git a/rust/protogen/src/main.rs b/rust/protogen/src/main.rs index c0412f18..11a176c2 100644 --- a/rust/protogen/src/main.rs +++ b/rust/protogen/src/main.rs @@ -34,10 +34,6 @@ fn main() -> Result<()> { let mut cfg = Config::new(); cfg.type_attribute(".", "#[non_exhaustive]"); - cfg.type_attribute( - "ommx.v1.Instance.Description", - "#[derive(::proptest_derive::Arbitrary)]", - ); cfg.out_dir(&out).compile_protos(&protos, &[proto_root])?; std::process::Command::new("rustfmt")