Some years ago, I worked on real-time meal delivery at Zoomer, a YC startup based out of Philadelphia. Zoomer’s production tech stack was primarily Ruby. As it grew we moved from using heuristics for things like routing and scheduling to open source optimization solvers.

Like most languages that aren’t Python, Ruby doesn’t have an especially mature ecosystem for optimization (or data science, or machine learning, for that matter). For some use cases that didn’t matter. When we upgraded the routing engine, we built a model in C++ using Gecode and wrapped a Ruby gem around a SWIG wrapper. But when we wanted to use integer programming to build schedules, the lack of solver APIs proved inconvenient.1

At the time, PuLP was probably the most commonly used open source multi-solver Python library for linear and integer programming.2 This led me the opportunity to develop RAMS, a PuLP-inspired library for basic MILP modeling in Ruby.

Then the Zoomer team became part of Grubhub. We moved to a Java stack and a commercial optimization solver. Improvements to the RAMS project languished on my todo list. It lagged behind major versions of Ruby, optimization, solvers, and dependencies, painfully out of date and unmaintained.

Then, last month, Github released its Copilot agent. Unlike vibe coding directly in the editor, which sounds like speeding maniacally through a bad acid trip, the idea here is more like running a project: create issues, receive and comment on pull requests, iterate.

I figured the grunt work of library upgrades should be perfect fodder to try out an AI developer assistant. RAMS is already well structured and tested. The upgrade is well defined. No creativity required.

A RAMS modeling example

This post is meandering through two topics: solving optimization models with Ruby and RAMS, and my experiences maintaining that library using Copilot. I could have split this into two posts, but that didn’t feel right. So let’s show what building a model in RAMS looks like first.

I don’t use Ruby with any regularity these days3, but modeling with RAMS reminded me how elegant Ruby DSLs can be. Here’s a simple example of a binary integer program.

#!/usr/bin/env ruby

require 'rams'

m = RAMS::Model.new

x1 = m.variable type: :binary
x2 = m.variable type: :binary
x3 = m.variable type: :binary

m.constrain(x1 + x2 + x3 <= 2)
m.constrain(x2 + x3 <= 1)

m.sense = :max
m.objective = 1 * x1 + 2 * x2 + 3 * x3

solution = m.solve
puts <<-HERE
objective: #{solution.objective}
x1 = #{solution[x1]}
x2 = #{solution[x2]}
x3 = #{solution[x3]}
HERE

I think that’s rather nice, and very clean.

RAMS enhancements

The biggest change in RAMS is that it now supports the HiGHS optimization solver. Prior to v0.2.0, GLPK was the default solver, but now that is HiGHS. There are a number of smaller changes as well.

  • RAMS requires Ruby v3.1.
  • CPLEX support was removed since I can’t test it.4
  • One can set solver paths using environment variables (e.g. RAMS_SOLVER_PATH_CBC).
  • Improved documentation and a logo!

The Copilot agent as coding companion

While I tend to err on the side of LLM skepticism, working with the Copilot agent for this upgrade was generally positive. It was a bit like working with a fast, responsive, and inexperienced developer. The issues it ran into were pretty much the same, but the time scale was compressed.

I had it open three pull requests for me.

🤨 PR 29: Upgrade Ruby and dependencies

Performance here was middling. Copilot got through some of the task without assistance. It also made a number of changes that were unhelpful and irrelevant to the request.

On a positive note, I forgot to ask it to change from CircleCI to GitHub Actions for testing. This gave me the opportunity to test its response to feature creep. It responded with a partially working GitHub Actions workflow (and no grumbling!).

Copilot made a number of errors and wasn’t able to finish the upgrade on its own.

🤩 PR 32: Add environment variables for solver paths

Copilot did a great job on this task. I had no issue with the code it wrote. It followed the style of the rest of the package nicely. It added appropriate documentation and unit tests.

👌 PR 34: Support the HiGHS optimization solver

Copilot did pretty well here, even though it didn’t get the feature working. It was able to create a new solver interface and get most of the logic for solution parsing right. I was a little surprised that it forgot to test the new solver integration in GitHub Actions. The biggest issue it needed my help on was solution status parsing, where it didn’t realize that the second condition here will never trigger.

return :feasible if status =~ /feasible/i
return :infeasible if status =~ /infeasible/i

This should have been the following (note the ^).

return :feasible if status =~ /^feasible/i
return :infeasible if status =~ /infeasible/i

  1. I don’t remember finding any MILP modeling interfaces for Ruby like PuLP in 2016-17. More recently, Rulp and Opt have been developed. ↩︎

  2. PulLP is still heavily used and developed↩︎

  3. Once upon a time I was a Perl programmer. Ruby was originally written to be a better Perl. I’ve long since given up the old ways. ↩︎

  4. For now, RAMS is focussing on open source solvers. Maintaining commercial solver licenses can be challenging when you’re not part of academia. PRs welcome. ↩︎