```json
{
  "sym_variables": [
    ("x0", "potatoes"),
    ("x1", "ham sandwiches"),
    ("x2", "bowls of instant ramen"),
    ("x3", "black beans")
  ],
  "objective_function": "4.31 * x0 + 8.98 * x1 + 5.94 * x2 + 6.61 * x3",
  "constraints": [
    "13 * x1 + 14 * x3 >= 48",
    "18 * x0 + 28 * x2 >= 20",
    "18 * x0 + 13 * x1 >= 43",
    "13 * x1 + 28 * x2 >= 23",
    "18 * x0 + 13 * x1 + 28 * x2 + 14 * x3 >= 23",
    "11 * x0 + 15 * x3 >= 44",
    "8 * x1 + 15 * x3 >= 41",
    "11 * x0 + 8 * x1 >= 32",
    "11 * x0 + 8 * x1 + 29 * x2 >= 31",
    "11 * x0 + 8 * x1 + 15 * x3 >= 31",
    "8 * x1 + 29 * x2 + 15 * x3 >= 31",
    "11 * x0 + 8 * x1 + 29 * x2 >= 44",
    "11 * x0 + 8 * x1 + 15 * x3 >= 44",
    "8 * x1 + 29 * x2 + 15 * x3 >= 44",
    "11 * x0 + 8 * x1 + 29 * x2 >= 49",
    "11 * x0 + 8 * x1 + 15 * x3 >= 49",
    "8 * x1 + 29 * x2 + 15 * x3 >= 49",
    "11 * x0 + 8 * x1 + 29 * x2 + 15 * x3 >= 49",
    "-5 * x2 + 8 * x3 >= 0",
    "18 * x0 + 28 * x2 + 14 * x3 <= 160",
    "18 * x0 + 13 * x1 + 28 * x2 <= 140",
    "11 * x0 + 8 * x1 + 15 * x3 <= 147",
    "11 * x0 + 29 * x2 + 15 * x3 <= 198",
    "18 * x0 + 13 * x1 + 28 * x2 + 14 * x3 <= 218",
    "11 * x0 + 8 * x1 + 29 * x2 + 15 * x3 <= 218"
  ]
}
```

```python
import gurobipy as gp

# Create a new model
m = gp.Model("food_optimization")

# Create variables
potatoes = m.addVar(lb=0, vtype=gp.GRB.CONTINUOUS, name="potatoes")
ham_sandwiches = m.addVar(lb=0, vtype=gp.GRB.CONTINUOUS, name="ham_sandwiches")
ramen = m.addVar(lb=0, vtype=gp.GRB.CONTINUOUS, name="ramen")
black_beans = m.addVar(lb=0, vtype=gp.GRB.CONTINUOUS, name="black_beans")

# Set objective function
m.setObjective(4.31 * potatoes + 8.98 * ham_sandwiches + 5.94 * ramen + 6.61 * black_beans, gp.GRB.MINIMIZE)

# Add constraints
m.addConstr(13 * ham_sandwiches + 14 * black_beans >= 48)
m.addConstr(18 * potatoes + 28 * ramen >= 20)
m.addConstr(18 * potatoes + 13 * ham_sandwiches >= 43)
m.addConstr(13 * ham_sandwiches + 28 * ramen >= 23)
m.addConstr(18 * potatoes + 13 * ham_sandwiches + 28 * ramen + 14 * black_beans >= 23)
m.addConstr(11 * potatoes + 15 * black_beans >= 44)
m.addConstr(8 * ham_sandwiches + 15 * black_beans >= 41)
m.addConstr(11 * potatoes + 8 * ham_sandwiches >= 32)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 29 * ramen >= 31)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 15 * black_beans >= 31)
m.addConstr(8 * ham_sandwiches + 29 * ramen + 15 * black_beans >= 31)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 29 * ramen >= 44)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 15 * black_beans >= 44)
m.addConstr(8 * ham_sandwiches + 29 * ramen + 15 * black_beans >= 44)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 29 * ramen >= 49)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 15 * black_beans >= 49)
m.addConstr(8 * ham_sandwiches + 29 * ramen + 15 * black_beans >= 49)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 29 * ramen + 15 * black_beans >= 49)
m.addConstr(-5 * ramen + 8 * black_beans >= 0)
m.addConstr(18 * potatoes + 28 * ramen + 14 * black_beans <= 160)
m.addConstr(18 * potatoes + 13 * ham_sandwiches + 28 * ramen <= 140)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 15 * black_beans <= 147)
m.addConstr(11 * potatoes + 29 * ramen + 15 * black_beans <= 198)

m.addConstr(18 * potatoes + 13 * ham_sandwiches + 28 * ramen + 14 * black_beans <= 218)
m.addConstr(11 * potatoes + 8 * ham_sandwiches + 29 * ramen + 15 * black_beans <= 218)


# Optimize model
m.optimize()

# Print results
if m.status == gp.GRB.OPTIMAL:
    print('Obj: %g' % m.objVal)
    for v in m.getVars():
        print('%s %g' % (v.varName, v.x))
elif m.status == gp.GRB.INFEASIBLE:
    print('Model is infeasible')
else:
    print('Optimization ended with status %d' % m.status)

```