Skip to content

Conversation

AnhTran01
Copy link
Contributor

Fixes NA

Summary/Motivation:

This PR introduced a new GAMS solver interface that uses the LinearRepnVisitor to enable coefficient gathering when writing out the scalar GAMS model in gms format.

Changes proposed in this PR:

  • New GAMS solver interface in pyomo.contrib.solver.solvers
  • New GAMS writer in pyomo.repn.plugins
  • New GAMS solution loader in pyomo.contrib.solver.solvers

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@AnhTran01
Copy link
Contributor Author

@boxblox
@abhosekar

@mrmundt
Copy link
Contributor

mrmundt commented Aug 7, 2025

@AnhTran01 - Please make sure to run black on your changes!

Copy link
Contributor

@mrmundt mrmundt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Immediate piece of feedback, recognizing that I have not thoroughly looked over anything - there are no tests anywhere, and there absolutely will need to be some.

Thanks for this, though, I am so excited!!

@@ -518,6 +518,11 @@ def write(self, model):
con_list[label] = declaration + definition + bounds
self.var_symbol_map.addSymbol(obj, label)

# Write the GAMS model
ostream.write("$offlisting\n")
# $offdigit ignores extra precise digits instead of erroring
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what was the error that you were seeing that prompted this addition? maybe something was being set above 1e300?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the previous commit test (b0c3450), the listing file output error message is "Too many digits in number $offdigit can be used to ignore trailing digits"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but do you know what triggered this? which test was causing this error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back to check the failed test. The following tests that triggered the "Too many digits error" are:

  1. TestSolvers.test_equality
  2. TestSolvers.test_linear_expression
  3. TestSolvers.test_mutable_param_with_range
  4. TestSolvers.test_param_changes
  5. TestSolvers.test_no_objective
  6. TestSolvers.test_time_limit
  7. TestLegacySolverInterface.test_param_updates

By testing locally, I see there are trailing decimal places and not the cases of 1e300 or large numbers.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Some initial comments, mostly on configuration)

self.writer_config: ConfigDict = self.declare(
'writer_config', GAMSWriter.CONFIG()
)
# Share the same config as the writer, passes at the function call write
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below: this should just insert a copy of the CONFIG from the writer. See Ipopt here for an example.

n,
)

def version(self, config=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our review of the new solver interfaces, we changed the API to:

def version(self, rehash: bool=False) -> Tuple:

Much like available(), this should leverage a cache (class dict attribute). You can look at the ipopt implementation for an idea (although that hasn't been updated to the new API).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify the purpose of using rehash and whether the user can call rehash from the solve statement?

I found the documentation here: https://pyomo.readthedocs.io/en/latest/api/pyomo.common.fileutils.PathManager.html

Would the implementation be Executable(config.executable.path()).rehash()?

if config.logfile is not None:
config.logfile = os.path.abspath(config.logfile)

config.writer_config.put_results_format = 'gdx' if gdxcc_available else 'dat'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably only overwrite the configuration if it is still None (that way users can set / override the interface)

@AnhTran01 AnhTran01 requested a review from mrmundt August 22, 2025 18:54
@AnhTran01
Copy link
Contributor Author

@mrmundt is the requested changes related to adding tests for the new solver interface/writer?

Comment on lines +121 to +130
# NOTE: Taken from the lp_writer
self.declare(
'row_order',
ConfigValue(
default=None,
description='Preferred constraint ordering',
doc="""
To use with ordered_active_constraints function.""",
),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand why this config option has been added. Shouldn't that option live under the writer?

EDIT: Aha, I see it also lives under the writer! Why is it in both places? I suspect this was a mistake.

Comment on lines +133 to +160
class GAMSResults(Results):
def __init__(self):
super().__init__()
self.return_code: ConfigDict = self.declare(
'return_code',
ConfigValue(default=None, description="Return code from the GAMS solver."),
)
self.gams_solver_termination_condition: ConfigDict = self.declare(
'gams_solver_termination_condition',
ConfigValue(
default=None,
description="Include additional TerminationCondition domain."
"Take precedence over model_termination_condition if interruption occur",
),
)
self.gams_model_termination_condition: ConfigDict = self.declare(
'gams_model_termination_condition',
ConfigValue(
default=None,
description="Include additional TerminationCondition domain.",
),
)
self.gams_solver_status: ConfigDict = self.declare(
'gams_solver_status',
ConfigValue(
default=None, description="Include additional SolverStatus domain."
),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsiirola and @michaelbynum - I think I would prefer that all of these items live under Results.extra_info rather than as distinct items / having a "custom" GAMSResults object. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no preference.

Comment on lines +86 to +129
Keyword Arguments
-----------------
output_filename: str
Name of file to write GAMS model to. Optionally pass a file-like
stream and the model will be written to that instead.
io_options: str
- warmstart=True
Warmstart by initializing model's variables to their values.
- symbolic_solver_labels=False
Use full Pyomo component names rather than
shortened symbols (slower, but useful for debugging).
- labeler=None
Custom labeler. Incompatible with symbolic_solver_labels.
- solver=None
If None, GAMS will use default solver for model type.
- mtype=None
Model type. If None, will chose from lp, nlp, mip, and minlp.
- add_options=None
List of additional lines to write directly
into model file before the solve statement.
For model attributes, <model name> is GAMS_MODEL.
- skip_trivial_constraints=False
Skip writing constraints whose body section is fixed.
- output_fixed_variables=False
If True, output fixed variables as variables; otherwise,
output numeric value.
- file_determinism=1
| How much effort do we want to put into ensuring the
| GAMS file is written deterministically for a Pyomo model:
- NONE (0) : None
- ORDERED (10): rely on underlying component ordering (default)
- SORT_INDICES (20) : sort keys of indexed components
- SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order)
- put_results='results'
Filename for optionally writing solution values and
marginals. If put_results_format is 'gdx', then GAMS
will write solution values and marginals to
GAMS_MODEL_p.gdx and solver statuses to
{put_results}_s.gdx. If put_results_format is 'dat',
then solution values and marginals are written to
(put_results).dat, and solver statuses to (put_results +
'stat').dat.
- put_results_format='gdx'
Format used for put_results, one of 'gdx', 'dat'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to sound weird coming from me but you do not need all of this extra documentation. The autodoc will handle it from the ConfigValue descriptions/docs. I would encourage you to delete this.

Comment on lines +264 to +265
def __init__(self):
self.config = self.CONFIG()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self):
self.config = self.CONFIG()
def __init__(self):
#: Instance configuration;
#: see :ref:`pyomo.repn.plugins.gams_writer_v2.GAMSWriter::CONFIG`.
self.config = self.CONFIG()


def __call__(self, model, filename, solver_capability, io_options):
if filename is None:
filename = 'GAMS_MODEL' + ".gms"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
filename = 'GAMS_MODEL' + ".gms"
filename = model.name + ".gms"

This is more standard.

Comment on lines +57 to +58
'Solution loader does not currently have a valid solution. Please '
'check results.termination_condition and/or results.solution_status.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +74 to +77
raise RuntimeError(
'Solution loader does not currently have a valid solution. Please '
'check results.termination_condition and/or results.solution_status.'
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this is supposed to be a different error from those in here: github.com/Pyomo/pyomo/blob/main/pyomo/contrib/solver/common/util.py

Comment on lines +102 to +103
'Solution loader does not currently have valid duals. Please '
'check results.termination_condition and/or results.solution_status.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +107 to +108
'Solution loader does not currently have valid duals. Please '
'check results.termination_condition and/or results.solution_status.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +135 to +143
raise NoReducedCostsError(
'Solution loader does not currently have valid reduced costs. Please '
'check results.termination_condition and/or results.solution_status.'
)
if self._gdx_data is None:
raise NoReducedCostsError(
'Solution loader does not currently have valid reduced costs. Please '
'check results.termination_condition and/or results.solution_status.'
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants