This task looks rather strange, there is not much context and some parts of the task might touch some not-so-nice areas of finite-domain based solvers (large domains or scaling / divisions during solving).
Therefore: consider this as an idea / template!
Code
from ortools.sat.python import cp_model# DataINPUT = 30000INPUT_UB = 1000000TAX_A = 11TAX_B = 30TAX_C = 41# Helpers# new variable which is constrained to be equal to: given input-var MINUS constant# can get negative / wrap-arounddef aux_var_offset(model, var, offset): aux_var = model.NewIntVar(-INPUT_UB, INPUT_UB, "") model.Add(aux_var == var - offset) return aux_var# new variable which is equal to the given input-var IFF >= 0; else 0def aux_var_nonnegative(model, var): aux_var = model.NewIntVar(0, INPUT_UB, "") model.AddMaxEquality(aux_var, [var, model.NewConstant(0)]) return aux_var# Modelmodel = cp_model.CpModel()# varssalary_var = model.NewIntVar(0, INPUT_UB, "salary")tax_component_a = model.NewIntVar(0, INPUT_UB, "tax_11")tax_component_b = model.NewIntVar(0, INPUT_UB, "tax_30")tax_component_c = model.NewIntVar(0, INPUT_UB, "tax_41")# constraintsmodel.AddMinEquality(tax_component_a, [ aux_var_nonnegative(model, aux_var_offset(model, salary_var, 10085)), model.NewConstant(25710 - 10085)])model.AddMinEquality(tax_component_b, [ aux_var_nonnegative(model, aux_var_offset(model, salary_var, 25711)), model.NewConstant(73516 - 25711)])model.Add(tax_component_c == aux_var_nonnegative(model, aux_var_offset(model, salary_var, 73516)))tax_full_scaled = tax_component_a * TAX_A + tax_component_b * TAX_B + tax_component_c * TAX_C# Demomodel.Add(salary_var == INPUT)solver = cp_model.CpSolver()status = solver.Solve(model)print(list(map(lambda x: solver.Value(x), [tax_component_a, tax_component_b, tax_component_c, tax_full_scaled])))
Output
[15625, 4289, 0, 300545]
Remarks
As implemented:
- uses scaled solving
- produces scaled solution (300545)
- no fiddling with non-integral / ratio / rounding stuff BUT large domains
Alternative:
- Maybe something around
AddDivisionEquality
Edit in regards to Laurents comments
In some scenarios, solving the scaled problem but being able to reason about the real unscaled values easier might make sense.
If i interpret the comment correctly, the following would be a demo (which i was not aware of and it's cool!):
Updated Demo Code (partial)
# Demo -> Attempt of demonstrating the objective-scaling suggestionmodel.Add(salary_var >= 30000)model.Add(salary_var <= 40000)model.Minimize(salary_var)model.Proto().objective.scaling_factor = 0.001 # DEFINE INVERSE SCALINGsolver = cp_model.CpSolver()solver.parameters.log_search_progress = True # SCALED BACK OBJECTIVE PROGRESSstatus = solver.Solve(model)print(list(map(lambda x: solver.Value(x), [tax_component_a, tax_component_b, tax_component_c, tax_full_scaled])))print(solver.ObjectiveValue()) # SCALED BACK OBJECTIVE
Output (excerpt)
......#1 0.00s best:30 next:[30,29.999] fixed_bools:0/1#Done 0.00s CpSolverResponse summary:status: OPTIMALobjective: 30best_bound: 30booleans: 1conflicts: 0branches: 1propagations: 0integer_propagations: 2restarts: 1lp_iterations: 0walltime: 0.0039022usertime: 0.0039023deterministic_time: 8e-08primal_integral: 1.91832e-07[15625, 4289, 0, 300545]30.0