GEMMA DEVELOPER AGENTSAVED EVIDENCE
← Dataset Exploration
ARCHIVED TRAINING / Pyomo/pyomo

pyomo__pyomo-3056

This is a preserved training example. Membership in the current training wave has not been verified.
Pyomo/pyomo · Task 19 of 169

Problem statement

Incorrect degrees of freedom with linear presolve
## Summary

Testing out `NLWriter().write(model, file, linear_presolve=True)`, I have noticed that sometimes a variable that is eliminated is still written to the nl file. 

### Steps to reproduce the issue

```python
import pyomo.environ as pyo 
import pyomo.repn.plugins.nl_writer as nl_writer
import subprocess

# We use one constraint to eliminate one variable,
# but apparently no variables get eliminated.

m = pyo.ConcreteModel()

m.x = pyo.Var([1, 2, 3], initialize=1, bounds=(0, 10))

m.subexpr = pyo.Expression(pyo.Integers)
m.subexpr[1] = m.x[1] + m.x[2]

m.eq = pyo.Constraint(pyo.Integers)
m.eq[1] = m.x[1] == 7
m.eq[2] = m.x[3] == 0.1 * m.subexpr[1] * m.x[2]

m.obj = pyo.Objective(expr=m.x[1]**2 + m.x[2]**2 + m.x[3]**3)

nl_fname = "test.nl"
with open(nl_fname, "w") as f:
    nlinfo = nl_writer.NLWriter().write(m, f, linear_presolve=True)

print(f"N. var = {len(nlinfo.variables)}")
print(f"N. con = {len(nlinfo.constraints)}")
print(f"N. eliminated = {len(nlinfo.eliminated_vars)}")

print(f"Eliminated: {nlinfo.eliminated_vars[0][0].name}")
print("Variables in nl file:")
for var in nlinfo.variables:
    print(f"  {var.name}")

# Not strictly necessary. This is just here to support the argument that
# the eliminated variable actually makes its way to the nl file.
subprocess.run(["ipopt", nl_fname])
```
This gives:
```console
N. var = 3
N. con = 1
N. eliminated = 1
Eliminated: x[1]
Variables in nl file:
  x[2]
  x[1]
  x[3]
```
Followed by the Ipopt log, which includes:
```console
Total number of variables............................:        3
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        3
                     variables with only upper bounds:        0
Total number of equality constraints.................:        1
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0
```
I would expect the model to have two variables and one constraint after presolve, where instead it (supposedly) has three variables and one constraint.

### Information on your system

Pyomo version: 6.7.0.dev0 (current main)
Python version: 3.9.6
Solver (if applicable): Ipopt 3.14.11 (should not be relevant)

### Hypothesis

This appears to be happening because the call to `NLWriter._categorize_vars` for named expressions is adds `expr_info.linear` to the `linear_by_comp` map, approximately here:
https://github.com/Pyomo/pyomo/blob/91729ba44cdbaa888eedc7072251015d1d9e8da2/pyomo/repn/plugins/nl_writer.py#L1637-L1650
Then, when running `_categorize_vars` for the constraint in which the subexpression participates, we add `linear_by_comp[subexpr_id]` to `nonlinear_vars` for the constraint. The core issue seems to be that we don't check whether any of the linear variables have been eliminated (i.e., are present in `linear_by_comp`) when recording the linear portion of components. However, I am not sure why this does not cause a problem for constraints (perhaps because they don't appear in other components' expressions - but maybe the wrong variable could still show up since the elimination is not applied to `expo_info.linear`?). 

I did not understand the substitution code well enough to propose a fix. In particular, it was not clear to me where the substitution is happening. I think I see it for the nonlinear case, but not for the linear case.

Training membership

opd_e4b_pure_v6b

Task issues extracted from preserved verified_rft_messages training examples. This is not a verified current RL wave manifest.

Task and snapshot provenance

Snapshot captured 2026-11-12T17:06:59.476467+00:00 · Autoresearch-Arena/gemma-4-developer-agent

{
  "datasets": [
    "opd_e4b_pure_v6b"
  ],
  "examples": 3,
  "instance_id": "pyomo__pyomo-3056",
  "problem_statement": "Incorrect degrees of freedom with linear presolve\n## Summary\r\n\r\nTesting out `NLWriter().write(model, file, linear_presolve=True)`, I have noticed that sometimes a variable that is eliminated is still written to the nl file. \r\n\r\n### Steps to reproduce the issue\r\n\r\n```python\r\nimport pyomo.environ as pyo \r\nimport pyomo.repn.plugins.nl_writer as nl_writer\r\nimport subprocess\r\n\r\n# We use one constraint to eliminate one variable,\r\n# but apparently no variables get eliminated.\r\n\r\nm = pyo.ConcreteModel()\r\n\r\nm.x = pyo.Var([1, 2, 3], initialize=1, bounds=(0, 10))\r\n\r\nm.subexpr = pyo.Expression(pyo.Integers)\r\nm.subexpr[1] = m.x[1] + m.x[2]\r\n\r\nm.eq = pyo.Constraint(pyo.Integers)\r\nm.eq[1] = m.x[1] == 7\r\nm.eq[2] = m.x[3] == 0.1 * m.subexpr[1] * m.x[2]\r\n\r\nm.obj = pyo.Objective(expr=m.x[1]**2 + m.x[2]**2 + m.x[3]**3)\r\n\r\nnl_fname = \"test.nl\"\r\nwith open(nl_fname, \"w\") as f:\r\n    nlinfo = nl_writer.NLWriter().write(m, f, linear_presolve=True)\r\n\r\nprint(f\"N. var = {len(nlinfo.variables)}\")\r\nprint(f\"N. con = {len(nlinfo.constraints)}\")\r\nprint(f\"N. eliminated = {len(nlinfo.eliminated_vars)}\")\r\n\r\nprint(f\"Eliminated: {nlinfo.eliminated_vars[0][0].name}\")\r\nprint(\"Variables in nl file:\")\r\nfor var in nlinfo.variables:\r\n    print(f\"  {var.name}\")\r\n\r\n# Not strictly necessary. This is just here to support the argument that\r\n# the eliminated variable actually makes its way to the nl file.\r\nsubprocess.run([\"ipopt\", nl_fname])\r\n```\r\nThis gives:\r\n```console\r\nN. var = 3\r\nN. con = 1\r\nN. eliminated = 1\r\nEliminated: x[1]\r\nVariables in nl file:\r\n  x[2]\r\n  x[1]\r\n  x[3]\r\n```\r\nFollowed by the Ipopt log, which includes:\r\n```console\r\nTotal number of variables............................:        3\r\n                     variables with only lower bounds:        0\r\n                variables with lower and upper bounds:        3\r\n                     variables with only upper bounds:        0\r\nTotal number of equality constraints.................:        1\r\nTotal number of inequality constraints...............:        0\r\n        inequality constraints with only lower bounds:        0\r\n   inequality constraints with lower and upper bounds:        0\r\n        inequality constraints with only upper bounds:        0\r\n```\r\nI would expect the model to have two variables and one constraint after presolve, where instead it (supposedly) has three variables and one constraint.\r\n\r\n### Information on your system\r\n\r\nPyomo version: 6.7.0.dev0 (current main)\r\nPython version: 3.9.6\r\nSolver (if applicable): Ipopt 3.14.11 (should not be relevant)\r\n\r\n### Hypothesis\r\n\r\nThis appears to be happening because the call to `NLWriter._categorize_vars` for named expressions is adds `expr_info.linear` to the `linear_by_comp` map, approximately here:\r\nhttps://github.com/Pyomo/pyomo/blob/91729ba44cdbaa888eedc7072251015d1d9e8da2/pyomo/repn/plugins/nl_writer.py#L1637-L1650\r\nThen, when running `_categorize_vars` for the constraint in which the subexpression participates, we add `linear_by_comp[subexpr_id]` to `nonlinear_vars` for the constraint. The core issue seems to be that we don't check whether any of the linear variables have been eliminated (i.e., are present in `linear_by_comp`) when recording the linear portion of components. However, I am not sure why this does not cause a problem for constraints (perhaps because they don't appear in other components' expressions - but maybe the wrong variable could still show up since the elimination is not applied to `expo_info.linear`?). \r\n\r\nI did not understand the substitution code well enough to propose a fix. In particular, it was not clear to me where the substitution is happening. I think I see it for the nonlinear case, but not for the linear case.",
  "repo": "Pyomo/pyomo",
  "source": "v2/experiments/train_data/opd_e4b_pure_v6b/verified_rft_messages.jsonl",
  "source_sha256": "0f62460a9706eea5e1605255896f191ab00cb5706c5f1f2e1c293e9a718218dc"
}
{
  "captured_at": "2026-11-12T17:06:59.476467+00:00",
  "membership": "archived training",
  "membership_evidence": "Task issues extracted from preserved verified_rft_messages training examples. This is not a verified current RL wave manifest.",
  "name": "Preserved post-training task archive",
  "source": "Autoresearch-Arena/gemma-4-developer-agent",
  "source_revision": "016496de222aaafca0d4ee4c18efc63ba616f76e",
  "sources": [
    {
      "path": "v2/experiments/train_data/opd_e4b_cispo_v5/verified_rft_messages.jsonl",
      "sha256": "a43610ad0dfb6ace95375cfae09000f41afc0338cc925af870a4ade7a9b20c4c"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_hybrid_v3/verified_rft_messages.jsonl",
      "sha256": "bd326303eb127e8e5e08f03daf97bb88d79d19709d59f3f93f348bc56686a8df"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_onpolicy_v3/verified_rft_messages.jsonl",
      "sha256": "960c680e797e823a0d3a22f4b1d5ef60a72971373d703b877340b0c9126aaf68"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_pure_v6a/verified_rft_messages.jsonl",
      "sha256": "f045b7ca5b3883a1e2eaee81e1c02a4a1b53dd7b7b36e98bacb2ba5823a15148"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_pure_v6b/verified_rft_messages.jsonl",
      "sha256": "0f62460a9706eea5e1605255896f191ab00cb5706c5f1f2e1c293e9a718218dc"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_pure_v7/verified_rft_messages.jsonl",
      "sha256": "e37f7bd675322f3f81500b73559d60d5f602ca0537bf19f5cb06332c728c7b72"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_pure_v7b/verified_rft_messages.jsonl",
      "sha256": "b864bdc8695183433eef6e31cb8d33fad44ba3aded2170c1ade4ad5b2944ce23"
    },
    {
      "path": "v2/experiments/train_data/opd_e4b_self_audit_v4/verified_rft_messages.jsonl",
      "sha256": "0026023745afb8f3dc1c176dc7f7171157b4ad5a66656b524af218e8e19ceee6"
    },
    {
      "path": "v2/experiments/train_data/rft_e4b_aligned_v2/verified_rft_messages.jsonl",
      "sha256": "92a4515fee9d152b0d4c510ee3ce4caaaffe2003ba7584469f8027f174aab18c"
    },
    {
      "path": "v2/experiments/train_data/rft_v1/verified_rft_messages.jsonl",
      "sha256": "a42453ccd7ac7680b0a03865e8e7080bc8bf74c8d9d193b440c441ed46f8512b"
    },
    {
      "path": "v2/experiments/train_data/rft_v2/verified_rft_messages.jsonl",
      "sha256": "18f4b3bd7e651f83c6acab479c9475e3fa50dc652a384f309e97146983ac85ba"
    }
  ]
}