feat(nl): parse + evaluate AMPL external functions (#15)#20
Merged
Conversation
Phase 1 of issue #15: ripopt's NL parser used to abort with `Unknown expression token: 'f0 3'` on any model that calls an AMPL imported function (e.g. IDAES property packages, CMarcher's Helmholtz flowsheet). That made those files unusable even for inspection. The parser now: - reads `nfunc` from dim line 4 (field 1) into `NlHeader.n_funcs`; - consumes top-level `F<k> <type> <nargs> <name>` segments into `NlFileData.imported_funcs`; - recognises `f<id> <nargs>` expression tokens and builds an `ExprNode::Funcall { id, args }` node; - walks past `f`-forms in `count_expr_lines_recursive` so surrounding expressions stay in sync. `NlProblem::from_nl_data` now returns `Result<Self, String>`. Before any tape is built it scans objective, constraint, and common expressions for `Funcall` nodes and, if any is found, resolves the id back to its `F` name and returns: problem uses external function '<name>'; external functions (AMPL imported functions) are not supported by ripopt Phase 2 (libloading + `funcadd_ASL` ABI + derivatives) is still open. Test fixtures: - tests/fixtures/issue_15/generate_helmholtz_nl.py — CMarcher's IDAES Helmholtz flowsheet (credit: CMarcher + GPT 5.4), regenerable with `idaes get-extensions` installed. - tests/fixtures/issue_15/idaes_helmholtz.nl — captured NL file with 3 `F` segments (vf_hp, h_liq_hp, h_vap_hp) and 4 `f<id>` calls. New integration tests (tests/nl_integration.rs): - minimal hand-written `f` in objective → clean error naming `myfunc`; - minimal hand-written `f` in a constraint → same; - real IDAES Helmholtz fixture → parse succeeds, all three F-names surface, construction errors with one of them named. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 2 of issue #15. Extends the NL loader beyond the clean-rejection stub so problems that reference AMPL imported functions (e.g. IDAES Helmholtz property calls) actually solve instead of erroring. - src/nl/external.rs: dlopen AMPLFUNC libraries via libloading, register funcs via funcadd_ASL, evaluate with value/gradient/packed-Hessian buffers. Process-wide Mutex guards load+call (AMPL libs keep mutable global state). Arity and string-arg type bits validated per call. - src/nl/autodiff.rs: new TapeOp::Funcall variant with Arc<ExternalLibrary>. Tape build resolves funcall ids through an ExternalResolver; forward, reverse, forward_tangent, hessian_accumulate, hessian_sparsity, and remap_op all handle Funcall. Second-order reverse uses packed upper- triangular Hessian from the library; sparsity emits self-product over real-arg vars. - src/nl/problem_impl.rs: resolve_externals walks the NL AST for funcall ids, loads each AMPLFUNC path as Arc<ExternalLibrary>, and maps each referenced name to a library. Clear error if AMPLFUNC unset or name missing. - tests/nl_integration.rs: nl_build_idaes_helmholtz_with_amplfunc end-to- end test. env_lock() serializes the two AMPLFUNC-touching tests so parallel execution does not leak env state. - examples/probe_external.rs: diagnostic that lists registered funcs from an AMPL external library. IDAES Helmholtz flowsheet fixture now solves to Optimal in 5 iterations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Helmholtz fixture exercises type=1 (string-arg) variadic functions only. Added three targeted tests against functions.dylib to cover the orthogonal axis: - eval_cbrt_matches_closed_form: verifies value, first, and second derivatives against cbrt's closed form at x=8 (2, 1/12, -1/144). - eval_cbrt_arity_mismatch_errors: exact positive nargs=1 rejects both underfilled (0) and overfilled (2) calls. - eval_cbrt_rejects_string_arg: strings passed to a type=0 function are rejected in our wrapper, not forwarded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jkitchin
added a commit
that referenced
this pull request
Apr 25, 2026
The v0.9 plan replaces the rmumps backend wholesale with feral, so
all rmumps-side recommendations in REFERENCE_GAP_ROADMAP.md are now
moot rather than deferred. Investing in rmumps internals
(CNTL(4) static-pivot threshold, ICNTL(24) null-pivot detection,
NBTINY/RINFOG telemetry, real MC64, METIS, 2x2 static pivots,
analysis-time front sizing, backward-error refinement stop) would
be wasted effort against code that is being removed.
Changes:
- Top-of-file status banner clarifying the post-v0.8 state and
listing the rmumps items that are moot (#3, #4, #7, #10, #11,
#15, #16, #17, #20).
- Each of those nine cross-cutting items is now wrapped in
strikethrough with a "MOOT (superseded by feral, v0.9)" tag.
- Closing summary rewritten: "the cross-cutting roadmap is
effectively closed; what's next is the feral swap and a
post-feral re-triage of the four known regressions
(CRESC50, OET4, OET6, TFI1) plus grid case30_ieee."
- Banner at the top of "Roadmap recommendations (rmumps-side)"
explaining that the full list below is preserved for historical
reference only.
Mechanics: ripopt's LinearSolver trait already abstracts the
backend, and MultifrontalLdl / IterativeMinres / HybridSolver are
all #[cfg(feature = "rmumps")]-gated. Feral slots in as another
LinearSolver impl with its own feature flag; the IPM driver does
not call rmumps directly. Telemetry/scaling/ordering responsibilities
that the moot items would have addressed are now feral's job.
No code changes; documentation only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jkitchin
pushed a commit
that referenced
this pull request
May 7, 2026
Add auxiliary preprocessing regression tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
F,f<id>) and reject with a clear error naming the library and symbol.funcadd_ASLABI, so problems that call imported functions (IDAES Helmholtz property packages, etc.) solve instead of erroring.Phase 2 design
src/nl/external.rs— dlopens AMPLFUNC libraries vialibloading, registers functions throughfuncadd_ASL/Addfunc, evaluates with value / gradient / packed-upper-triangular Hessian. A process-wideMutexguards load+call because AMPL libs keep mutable global state. Arity and string-arg type bits are validated per call.src/nl/autodiff.rs— newTapeOp::Funcallvariant holdingArc<ExternalLibrary>. Every tape pass handles it:forward,reverse(chain rule throughderivs[k]),forward_tangent,hessian_accumulate(forward-over-reverse with the packed Hessian),hessian_sparsity(self-product over real-arg vars),remap_op.src/nl/problem_impl.rs—resolve_externalswalks the NL AST for funcall ids, loads each path onAMPLFUNC(colon- or newline-separated) asArc<ExternalLibrary>, and maps referenced names to a library. Clean error ifAMPLFUNCis unset or a required symbol is missing.examples/probe_external.rs— diagnostic that lists functions registered by a library.Result
Test coverage
nl_parse_external_function_*funcadd_ASLload_idaes_helmholtz_dylib_registers_known_functionsAMPLFUNCunsetnl_parse_idaes_helmholtz_fixturenl_build_idaes_helmholtz_with_amplfunc+ CLI smoketype=1(string args) + variadicnargs<0eval_vf_hp_*against IDAES Helmholtzvf_hptype=0(no strings) + exactnargs>0eval_cbrt_matches_closed_formagainst IDAESfunctions.dylibeval_cbrt_matches_closed_formeval_cbrt_arity_mismatch_errorseval_cbrt_rejects_string_argKnown gaps — not exercised by any test
These paths are either not triggered by any AMPL library in our environment, or rely on optional AMPL ABI features we have not had a fixture for. The code may be wrong on these axes and we would not catch it:
FUNCADD_OUTPUT_ARGS(type bit 2) — functions that return extra values through output slots instead of the return value. Oureval()accepts the type bit but does not wire the output path back to the caller. Likely broken if encountered.FUNCADD_RANDOM_VALUED(type bit 4) — RNG functions. Not handled at all; unusual in optimization NL files but possible.AMPLFUNC— the splitter is implemented but no test exercises a model that needs symbols from two libraries at once.errmsg— we read the buffer and surface it as anErr, but no test triggers a library to write into it.nargs=-4, i.e. min 3). More extreme arities are not directly covered.If you hit one of these in a real model, please open an issue with the library and
.nlso we can add a fixture.Test plan
cargo test— 259/259 passnl_parse_external_function_reports_clean_error— rejection path still works when AMPLFUNC is absentnl_parse_idaes_helmholtz_fixture— clean error when AMPLFUNC is unset (serialized viaenv_lock())nl_build_idaes_helmholtz_with_amplfunc— end-to-end build with AMPLFUNC setripoptCLI solvestests/fixtures/issue_15/idaes_helmholtz.nltoOptimalfunctions.dylibcovertype=0, exact arity, string-arg rejectioncc @CMarcher — phase 2 is ready. If you have a Helmholtz flowsheet
.nlhandy, you can solve it with:Multiple libraries work with colon- or newline-separated paths in
AMPLFUNC.Closes #15.
🤖 Generated with Claude Code