惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
D
Docker
有赞技术团队
有赞技术团队
D
DataBreaches.Net
The GitHub Blog
The GitHub Blog
爱范儿
爱范儿
H
Help Net Security
美团技术团队
MyScale Blog
MyScale Blog
B
Blog RSS Feed
C
Check Point Blog
Microsoft Security Blog
Microsoft Security Blog
阮一峰的网络日志
阮一峰的网络日志
A
About on SuperTechFans
小众软件
小众软件
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
G
Google Developers Blog
月光博客
月光博客
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Blog — PlanetScale
Blog — PlanetScale
MongoDB | Blog
MongoDB | Blog
F
Fortinet All Blogs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Why I made OR-Tools prove it was better than the determin...
Kingsley Onoh · 2026-06-16 · via DEV Community

Dispatch optimization needs a lower bound before it needs a clever objective.

In the first real OR-Tools integration, the solver selected fewer assignments than the deterministic fallback it needed to improve. That result made the boundary explicit: CP-SAT could optimize cost, priority, and tie-breakers only after it matched or beat the deterministic feasible assignment count.

The constraint changed how I treated OR-Tools inside the dispatch engine. I had treated the solver as the smarter engine in the room. The code reminded me that dispatch combines math with an operating record. A plan has timestamps, frozen work, post-selection capacity checks, replay metrics, and explanations a dispatcher can defend after the board changes.

The tempting version

The tempting version is simple. Build one boolean variable per eligible technician-job decision. Add constraints for job uniqueness, technician capacity, planning windows, and frozen work. Maximize the objective. Return the result.

That version reads well in a design doc. It is also too trusting for dispatch.

A field-service board has commitments. A dispatcher accepts a plan. A technician starts driving. A supervisor freezes a job. A customer is waiting against an SLA clock. If the solver returns an answer that is mathematically feasible but operationally worse, the system still has to notice.

The code that changed the contract

The final adapter runs deterministic solving first, uses that count as a lower bound, and then lets CP-SAT optimize within that boundary.

val vars = linearArgs(decisions.map(_.variable))
val deterministicAssignmentCount = fallback.solve(model, options).assignments.size
if (deterministicAssignmentCount > 0) {
  val _ = cp.addGreaterOrEqual(LinearExpr.sum(vars), deterministicAssignmentCount.toLong)
}
val coeffs = decisions.map { decision =>
  val assignmentReward = 1_000_000L - (decision.job.priority.rank.toLong * 10_000L)
  val cost = (decision.cost.total * BigDecimal(100))
    .setScale(0, BigDecimal.RoundingMode.HALF_UP)
    .toLong
  assignmentReward - cost - jobIndex(decision.job.id).toLong * 100L -
    technicianIndex(decision.technician.id).toLong
}.toArray
cp.maximize(LinearExpr.weightedSum(vars, coeffs))

That sample is from OrToolsSolverAdapter.solveWithCpSat. The assignment reward is intentionally large. Priority affects the reward. Cost is scaled to an integer. Job and technician indexes act as stable tie-breakers.

The line that matters most is not the maximize call. It is cp.addGreaterOrEqual(LinearExpr.sum(vars), deterministicAssignmentCount.toLong). That line says the solver is allowed to optimize, but it is not allowed to schedule less work than the deterministic feasible path already found.

Why deterministic sequencing stayed

Even after CP-SAT selects decisions, the system does not blindly stamp them into the board. It passes selected decisions through deterministic scheduling. That second stage can still reject work for capacity or planning-window overflow.

At first, that felt redundant. If the solver has constraints, why check again?

Because the dispatch plan is not only a set of pairs. It is a sequence of visits with concrete start times, travel, overtime, and explanation codes. Stable timestamps matter for replay. Stable rejection reasons matter for support. The deterministic layer turns selected pairs into an operating plan that looks the same when the same input snapshot is replayed.

That also protects partial plans. A solver timeout or infeasible slice should not fabricate certainty. The domain has reason codes such as missing_capability, frozen_assignment, capacity_exceeded, outside_planning_window, and solver_timeout. A partial plan with honest unscheduled work is safer than a complete-looking plan built on silence.

Frozen work was the real domain invariant

The solver failure was loud because it affected assignment count. Frozen work is quieter and more dangerous.

The constraint builder treats accepted, completed, and frozen assignments as hard facts. A technician who conflicts with frozen work is rejected. A job that would collide with preserved work does not get moved just because the global objective improves.

That choice is easy to miss if you only look at optimization. A solver optimizes variables. Dispatchers manage promises. Once a human has accepted work, the board has a memory. The optimizer has to respect that memory.

What surprised me

The surprise was not that OR-Tools needed constraints. That is normal. The surprise was that the deterministic implementation became a guardrail for the solver rather than dead code waiting to be deleted.

I kept it for three reasons.

First, it gives the CP-SAT model a feasible assignment lower bound. Second, it gives the app a fallback when native solver loading, runtime failure, or timeout happens. Third, it gives replay a baseline that operators can compare against using SLA hit rate, travel minutes, overtime minutes, churn moves, unscheduled jobs, and solve time.

That makes the deterministic path part of the product, not a temporary scaffold.

The tradeoff

The cost is extra machinery. There are two solve paths. There is trace metadata. There are post-selection checks. There are tests that assert OR-Tools was invoked, no fallback happened, and deterministic results still match where they should.

The benefit is that optimization no longer gets special trust. It has to earn its place inside the operating record.

That is the lesson I took from this build: in systems that move real work, a smarter algorithm is not automatically the source of truth. Sometimes the older deterministic code is the witness that keeps the new optimizer honest.