<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <atom:link href="http://kitchingroup.cheme.cmu.edu/blog/feed/index.xml" rel="self" type="application/rss+xml"/>
    <title>The Kitchin Research Group</title>
    <link>https://kitchingroup.cheme.cmu.edu/blog</link>
    <description>Chemical Engineering at Carnegie Mellon University</description>
    <pubDate>Fri, 30 Oct 2026 23:57:56 GMT</pubDate>
    <generator>Quarto + legacy Blogofile feed bridge</generator>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    
    <item><title>How we do agentic science</title><link>https://kitchingroup.cheme.cmu.edu/posts/agentic-science/</link><pubDate>Thu, 29 Oct 2026 00:00:00 +0000</pubDate><category>ai</category><category>agents</category><category>scimax</category><category>optimization</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/agentic-science/</guid><description>How we do agentic science</description><content:encoded><![CDATA[ 

Skip to main content




<p>For more than 25 years I have built the tools that make the environment I want to work in: Python packages for molecular simulation, Emacs and org-mode, org-ref, scimax, pycse, and most recently scimax for VS Code and a stack of optimization software. AI has not changed that for me. You might think of me as a software beaver, and it changed how big a dam one person can build. In the past year there were about 10,400 commits across scimax, discopt, pounce, feral and difflow, and about 76% of them were co-authored with an AI coding agent. I wrote far fewer of the lines than I used to. I wrote the specifications, I reviewed the changes, and I wrote or approved the tests that decide whether a change is accepted.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/software-beaver.svg" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:55.0%"></p>
</figure>
</div>
<p>That shift is the subject of this post. When an agent can write code, read papers and run calculations, the bottleneck moves. It used to be typing code; now it is deciding what to build and verifying that it is right. I have written about this before in the context of self-driving laboratories <a href="https://kitchingroup.cheme.cmu.edu/papers/kitchin2025b/index.html">(Kitchin, 2025)</a>, and with Hongliang Xin and Heather Kulik about what agentic science could look like more broadly <a href="https://kitchingroup.cheme.cmu.edu/papers/xin2025/index.html">(Xin, Kitchin and Kulik, 2025)</a>. Here I want to be concrete in how we are implementing these ideas. Our approach is mostly embodied in software, so this is a tour of that software and of the ideas that hold it together.</p>
<section id="an-agent-is-only-as-good-as-its-environment" class="level2">
<h2 class="anchored" data-anchor-id="an-agent-is-only-as-good-as-its-environment">An agent is only as good as its environment</h2>
<p>The simplest way to describe our approach is that we spend our effort on the environments that we work in and that the agent works in, rather than on the agent itself. Three ideas run through everything below.</p>
<p>The first is that plain text is ready for agents. Our notes, code, data and manuscripts live in org-mode and Markdown files that an agent can read, search and edit with the same tools it uses on source code. Nothing has to be exported or scraped first.</p>
<p>The second is that abilities should be installable. When an agent needs to know how to set up a VASP calculation or diagnose a stalled interior point solve, that knowledge should not live in a prompt someone pasted once. It should ship as a skill or an MCP server, versioned alongside the code it describes, and be installable with one command.</p>
<p>The third, and the most important, is that software should report its work. Agents are fluent, but fluency is not correctness. The tools we build for agents return structured results, validate their input, raise typed errors instead of returning a plausible wrong answer, and are tested against references we trust. That gives us something to check the agent’s claims against.</p>
<p>The diagram shows how the pieces fit together. You set the goals and check the results. The agent does the work through abilities that knowhow installs, using four groups of tools. Underneath is plain text, and around all of it are checks the agent cannot talk its way past. The rest of the post follows the diagram from top to bottom.</p>
<style>
.stack-fig { --ink:#142033; --muted:#5b6b82; --line:#c9d6e8; --card:#f3f7fd; --band:#dbe8f8; --accent:#2563b8; --guard:#fdf3e7; --guard-line:#d9822b; }
body.quarto-dark .stack-fig { --ink:#dbe5f3; --muted:#94a6c0; --line:#2b3d5a; --card:#101b2e; --band:#16243b; --accent:#7cb4ff; --guard:#2a1f12; --guard-line:#e0a060; }
.stack-fig svg { width:100%; height:auto; font-family:"IBM Plex Sans",system-ui,sans-serif; }
.stack-fig .box { fill:var(--card); stroke:var(--line); stroke-width:1.2; }
.stack-fig .band { fill:var(--band); stroke:var(--line); stroke-width:1.2; }
.stack-fig .agent { fill:var(--card); stroke:var(--accent); stroke-width:2; }
.stack-fig .guard { fill:var(--guard); stroke:var(--guard-line); stroke-width:1.5; }
.stack-fig text { fill:var(--ink); font-size:13px; }
.stack-fig .h { font-weight:600; font-size:15px; }
.stack-fig .pkg { font-weight:600; fill:var(--accent); }
.stack-fig .m { fill:var(--muted); font-size:12.5px; }
.stack-fig .code { font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:11.5px; fill:var(--muted); }
.stack-fig .arrow { stroke:var(--muted); stroke-width:1.5; fill:none; marker-end:url(#stack-arrow); }
.stack-fig .arrowhead { fill:var(--muted); }
</style>
<figure class="stack-fig figure">
<svg viewbox="0 0 760 470" aria-labelledby="stack-title stack-desc">
<title id="stack-title">Our agentic science stack</title>
<desc id="stack-desc">A person sets goals and checks results. An AI agent works through capabilities installed by knowhow, using four groups of tools: scimax for VS Code, litdb and crucible, fairchem-mcp and discopt-calphad, and difflow, pounce, discopt and plugins. All of it rests on plain-text files and is checked by tests, benchmarks, schema validation and citation checks.</desc>
<defs><marker id="stack-arrow" viewbox="0 0 10 10" refx="9" refy="5" markerwidth="7" markerheight="7" orient="auto-start-reverse"><path class="arrowhead" d="M0,0 L10,5 L0,10 z"></path></marker></defs>

<rect class="box" x="10" y="8" width="740" height="40" rx="8"></rect>
<text class="h" x="380" y="33" text-anchor="middle">You: set goals, decide what matters, check results</text>

<path class="arrow" d="M330,50 L330,72"></path>
<text class="m" x="322" y="65" text-anchor="end">specs and goals</text>
<path class="arrow" d="M430,72 L430,50"></path>
<text class="m" x="438" y="65">results and evidence</text>

<rect class="agent" x="10" y="76" width="740" height="40" rx="8"></rect>
<text class="h" x="380" y="101" text-anchor="middle">AI agent (Claude Code, Codex, Gemini, …)</text>

<rect class="band" x="10" y="124" width="740" height="36" rx="8"></rect>
<text x="380" y="147" text-anchor="middle"><tspan class="pkg">knowhow</tspan>  installs skills · subagents · commands · hooks · MCP servers</text>

<path class="arrow" d="M98,160 L98,180"></path>
<path class="arrow" d="M286,160 L286,180"></path>
<path class="arrow" d="M474,160 L474,180"></path>
<path class="arrow" d="M662,160 L662,180"></path>

<rect class="box" x="10" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="98" y="208" text-anchor="middle">Workbench</text>
<text class="pkg" x="98" y="230" text-anchor="middle">scimax for VS Code</text>
<text class="m" x="98" y="254" text-anchor="middle">notes · agenda · code</text>
<text class="m" x="98" y="272" text-anchor="middle">cite · publish · slides</text>
<text class="code" x="98" y="314" text-anchor="middle">CLI + 4 skills</text>

<rect class="box" x="198" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="286" y="208" text-anchor="middle">Knowledge</text>
<text class="pkg" x="286" y="230" text-anchor="middle">litdb · crucible</text>
<text class="m" x="286" y="254" text-anchor="middle">search the literature</text>
<text class="m" x="286" y="272" text-anchor="middle">papers → linked org wiki</text>
<text class="code" x="286" y="314" text-anchor="middle">MCP + JSON CLI</text>

<rect class="box" x="386" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="474" y="208" text-anchor="middle">Computation</text>
<text class="pkg" x="474" y="230" text-anchor="middle">fairchem-mcp</text>
<text class="pkg" x="474" y="248" text-anchor="middle">discopt-calphad</text>
<text class="m" x="474" y="272" text-anchor="middle">steerable simulations</text>
<text class="m" x="474" y="290" text-anchor="middle">certified phase diagrams</text>
<text class="code" x="474" y="314" text-anchor="middle">MCP + certificates</text>

<rect class="box" x="574" y="184" width="176" height="150" rx="8"></rect>
<text class="h" x="662" y="208" text-anchor="middle">Optimization</text>
<text class="pkg" x="662" y="230" text-anchor="middle">difflow · pounce</text>
<text class="pkg" x="662" y="248" text-anchor="middle">discopt + plugins</text>
<text class="m" x="662" y="272" text-anchor="middle">flowsheets · NLP · MINLP</text>
<text class="code" x="662" y="314" text-anchor="middle">MCP + solve reports</text>

<rect class="band" x="10" y="346" width="740" height="40" rx="8"></rect>
<text x="380" y="371" text-anchor="middle"><tspan font-weight="600">Plain text underneath:</tspan> org · Markdown · BibTeX · JSON reports</text>

<rect class="guard" x="10" y="398" width="740" height="62" rx="8"></rect>
<text class="h" x="380" y="422" text-anchor="middle">Checks the agent cannot talk its way past</text>
<text class="m" x="380" y="444" text-anchor="middle">tests · oracle comparisons · public benchmarks · schema validation · citation checks</text>
</svg>
</figure>

</section>
<section id="the-workbench-scimax-for-vs-code" class="level2">
<h2 class="anchored" data-anchor-id="the-workbench-scimax-for-vs-code">The workbench: scimax for VS Code</h2>
<p><a href="https://github.com/jkitchin/scimax_vscode">scimax for VS Code</a> is where the work happens. It brings the scimax environment I built in Emacs to VS Code, and it is one plain-text environment for the whole research cycle. I think in it, with a journal, notes, an agenda, and a searchable database of every org file I have. I compute in it, with runnable Python and Jupyter code blocks inside the document. I write in it, with org-ref citations, BibTeX tools and manuscript publishing for journal submission. I publish from it, and I present from it, using Marp decks with a pen, a laser pointer, a timer and live Python.</p>
<p>Because all of this is plain text, an agent can work with it as easily as I can. A TODO is just a headline in an org file, and scimax gathers those headlines into an agenda. Claude can read that agenda and update it too.</p>
<div class="quarto-layout-panel" data-layout-ncol="2">
<div class="quarto-layout-row">
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/scimax-org-todo.png" class="img-fluid figure-img"></p>
<figcaption>TODOs are plain-text headlines in an org file…</figcaption>
</figure>
</div>
</div>
<div class="quarto-layout-cell" style="flex-basis: 50.0%;justify-content: flex-start;">
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/scimax-agenda.png" class="img-fluid figure-img"></p>
<figcaption>…that scimax gathers into an agenda (demo data).</figcaption>
</figure>
</div>
</div>
</div>
</div>
<p>The agent-facing part of scimax is a command-line tool and four skills. The <code>scimax</code> CLI covers the agenda, search, the org database, the journal, tasks and projects, citation checking, export, tangling, publishing and slide builds. <code>scimax skill install</code> adds skills that teach an agent how to use it, so it can act as an executive assistant over my notes: a morning briefing, triaging tasks, finding what is blocked, or working out the critical path through a project. Sites built with <code>scimax publish</code> can include an “Ask” chat that finds the relevant sections and writes an answer with citations, using a language model that runs in the reader’s browser, so there is no server to maintain. This site has one. scimax for VS Code has had 838 commits since January 2026, about 72% of them co-authored with AI.</p>
</section>
<section id="giving-agents-abilities-knowhow" class="level2">
<h2 class="anchored" data-anchor-id="giving-agents-abilities-knowhow">Giving agents abilities: knowhow</h2>
<p>The skills that come with scimax are one example of a broader pattern, and <a href="https://github.com/jkitchin/knowhow">knowhow</a> is how we manage it. knowhow is a package manager for agent capabilities. <code>uvx knowhow add owner/repo --skill name</code> installs skills, slash commands, subagents, hooks and MCP servers from any git repository. It works with Claude Code, Codex, OpenCode, Gemini, Cursor and Copilot, and it records what it installed in a lock file so a project’s agent setup can be reproduced. knowhow replaces an earlier tool of ours called skillz.</p>
<p>knowhow also carries a library of our own: 44 skills for scientific, laboratory and programming work (ASE, VASP, pymatgen, Opentrons robots, design of experiments, electronic lab notebooks and scientific review, among others), along with subagents, commands and hooks. Some of the hooks are guardrails rather than abilities. One keeps an automatic lab-notebook record of what the agent did, and another keeps it from writing secrets into files.</p>
<p>The most useful thing about knowhow may be the convention it set. Every package in the rest of this post ships its own skill in <code>skills/&lt;name&gt;/SKILL.md</code>, next to the code it describes. The instructions change when the code changes, and <code>knowhow add</code> can install any of them.</p>
</section>
<section id="knowing-what-we-know-litdb-and-crucible" class="level2">
<h2 class="anchored" data-anchor-id="knowing-what-we-know-litdb-and-crucible">Knowing what we know: litdb and crucible</h2>
<p>Research starts with the literature, and agents are notorious for inventing it. Our answer is to give the agent a library that we curate, so it searches real papers instead of what it half-remembers.</p>
<p><a href="https://github.com/jkitchin/litdb">litdb</a> is a local database of the papers I care about, with natural-language search. Its MCP server is read-only by design. An agent can run semantic and full-text searches, find papers similar to one it already has, pull new work from OpenAlex, and return citations and BibTeX entries that point to real records. It can even generate the collaborator list an NSF proposal requires, which is the kind of tedious, checkable task that suits an agent well.</p>
<p><a href="https://github.com/jkitchin/crucible">crucible</a> works one level up. An LLM distills papers, notebooks and data into an interlinked org-mode wiki, one article per concept, with an SQLite graph of the links behind it. Ingesting happens in two steps: the agent first proposes which articles to write or update, then writes them. Once the wiki exists, an agent can use the crucible CLI to search it, follow backlinks, find related articles, and look for gaps such as orphan articles and sources that have not been distilled yet. <code>crucible publish</code> turns the wiki into a website using scimax.</p>
<p>These tools are not wired together in code, but they make one workflow. litdb finds the papers, crucible distills them into concepts, and scimax is where I write with them and publish the result. org-ref citation keys run through all three, so a claim in a draft can be traced back to a paper in the database.</p>
</section>
<section id="computing-with-agents-fairchem-mcp-and-discopt-calphad" class="level2">
<h2 class="anchored" data-anchor-id="computing-with-agents-fairchem-mcp-and-discopt-calphad">Computing with agents: fairchem-mcp and discopt-calphad</h2>
<p>Most atomistic simulation workflows were designed for people. You write a script, submit it, wait, and read the output. When an agent drives that workflow, it does the same thing in a loop: write a script, run it, wait, read a log file, and guess what went wrong. Every step reloads the model, and the agent finds out that a calculation went badly only after it is over. We wanted computation that an agent could work with interactively, and results that carry their own evidence of correctness.</p>
<section id="watching-and-steering-simulations" class="level3">
<h3 class="anchored" data-anchor-id="watching-and-steering-simulations">Watching and steering simulations</h3>
<p><a href="https://github.com/jkitchin/fairchem-mcp">fairchem-mcp</a> is an MCP server for the FAIRChem UMA and eSEN <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/neural-network-potentials/index.html">machine-learned potentials</a>, built on ASE. It loads a model once and keeps it in memory, so every later call reuses it instead of spending seconds to minutes loading it again. The agent builds or loads a structure and starts a job: a relaxation, a molecular dynamics run, an NEB calculation, an equation-of-state or elastic scan, phonons, a formation-energy convex hull, or a saddle-point or minima search. Each job runs in the background.</p>
<p>The interesting part is what happens while a job runs. <code>get_status</code> reports the step, the energy and the maximum force, along with a trend label: decreasing, plateaued, stuck or diverging. That label is written for an agent. It turns a column of numbers into a decision about whether to intervene. If a relaxation is stuck, the agent can switch the optimizer partway through, and the atoms keep their positions; only the driver is rebuilt. It can also tighten the force criterion, change the temperature of an MD run, turn on climbing in an NEB, or pause or abort the job. The skill that comes with the server teaches this start, watch and steer loop, including how to respond to each trend label and which job answers which question.</p>
<p>Two other design choices matter. The high-level tools and an <code>execute</code> escape hatch share one Python namespace, so when no tool fits, the agent can drop into Python on the same live <code>Atoms</code> object the job is using. And <code>introspect</code> reads the signatures and docstrings of the code that is actually installed, rather than documentation the model may remember from an older version. That removes a common source of agent errors: calling functions with arguments they no longer take. Because <code>execute</code> runs arbitrary code, this is a tool for a trusted local machine.</p>
<p>The examples in the repository are end-to-end workflows, each written both as a runnable script and as a sequence of MCP tool calls: adsorption energies, adsorption-site searches, diffusion barriers, surface phonons, and an alloy design loop that screens candidates for stability on the convex hull and then ranks the survivors by stiffness. Three routes to transition states, the dimer method, Sella, and eigenvector following in pounce, run on the same system so they can be compared. Installing all of it is one command:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">uvx</span> knowhow add jkitchin/fairchem-mcp <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--skill</span> fairchem-mcp <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--mcp</span> fairchem</span></code></pre></div></div>
</section>
<section id="from-energies-to-certified-phase-diagrams" class="level3">
<h3 class="anchored" data-anchor-id="from-energies-to-certified-phase-diagrams">From energies to certified phase diagrams</h3>
<p>A machine-learned potential gives energies of atomic configurations at 0 K. Materials questions are often about <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/alloy-phase-diagrams/index.html">phase diagrams</a>, which need much more: the Gibbs energy of every competing phase as a function of composition and temperature, including the liquid. <a href="https://github.com/jkitchin/discopt-calphad">discopt-calphad</a> connects the two. It is a discopt plugin that computes CALPHAD phase equilibria, and it can compute formation and mixing enthalpies from any ASE calculator, UMA included, and refit the enthalpy terms of an existing thermodynamic database to them. It does not replace the database’s entropy and heat-capacity terms, which a 0 K calculation cannot supply. The agent can then map the phase diagram from the refitted database, compare it with the original, and see what the potential changed.</p>
<p>What makes discopt-calphad suitable for agents is that its answers are certified. A conventional equilibrium calculation finds a local minimum of the Gibbs energy and cannot tell you whether a more stable phase or a miscibility gap was missed. discopt-calphad solves the equilibrium locally, then uses discopt’s global solver to check every phase against the result. If any phase could lower the energy, it is added and the equilibrium is solved again. If none can, the result is certified to within a stated tolerance. Each result carries a certificate level, and when the search cannot finish, the result says so instead of reporting an uncertified answer as if it were certified. In Cu–O, it finds a liquid miscibility gap that a grid-based calculation misses. An agent does not have to judge whether a phase diagram looks plausible. It can read the certificate.</p>
<p>Both of these tools use the optimization software described next: pounce for one of the transition-state searches, and discopt for the certification.</p>
</section>
</section>
<section id="solvers-that-explain-themselves" class="level2">
<h2 class="anchored" data-anchor-id="solvers-that-explain-themselves">Solvers that explain themselves</h2>
<p><img src="https://kitchingroup.cheme.cmu.edu/posts/agentic-science/img/optimization-landscape.png" class="img-fluid"></p>
<p>Since December 2025 we have started 15 public repositories in <a href="https://kitchingroup.cheme.cmu.edu/papers/kitchin2025/index.html">differentiable programming</a> and optimization. Together they hold about 1.8 million lines of source and tests, roughly 1.1 million in Python and 0.66 million in Rust, and they have had about 63,000 downloads from PyPI in the last 180 days. Projects like these usually take a team years. The map above, from an <a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/">earlier post</a>, shows how they relate.</p>
<p>Optimization turns out to be a good domain for AI-assisted development because the answers can be checked. A reported optimum either satisfies the optimality conditions and matches the reference solution, or it does not, and there are large public benchmark sets that say what the right answer is. The same property makes these solvers good tools for agents to use, as long as the solvers explain what they did. That was a design goal from the start.</p>
<p><a href="https://github.com/jkitchin/differentiable-flowsheets">difflow</a> is a JAX framework for differentiable chemical process flowsheets, for simulation, optimization and technoeconomic analysis. Its MCP server lets an agent build a flowsheet one step at a time: choose the species, add units, connect streams and set feeds, with undo and redo when it makes a mistake. Then it can solve the flowsheet, run sensitivity analyses and sweeps, optimize it, propagate uncertainty, and estimate costs. When a flowsheet does not converge, which is the normal state of a new flowsheet, the diagnosis tools return structured findings and suggested remedies rather than a stack trace, and <code>converge</code> tries the remedies in turn. Each tool is labeled as reading, editing or executing code, and one server configuration removes arbitrary Python execution entirely, for when you want the agent to stay inside the modeling tools. difflow can use pounce and discopt as its solvers.</p>
<p><a href="https://github.com/jkitchin/pounce">pounce</a> is a family of interior point solvers written in Rust. It began as a port of Ipopt and now covers nonlinear, conic and quadratic programs. Every solve writes a JSON solve report, so an agent reads structured data instead of parsing an iteration log. The pounce-studio MCP server has about 30 tools built on those reports. Before a solve, an agent can analyze the problem and check the starting point. After a solve, it can diagnose the result, find where progress stalled, look at the convergence trace and the restoration phases, compare two runs, and verify the solution independently. If that is not enough, it can step through a solve in a live debugger. pounce also includes commands for adversarial testing and for reviewing sensitivity results.</p>
<p><a href="https://github.com/jkitchin/discopt">discopt</a> is a modeling language and solver for mixed-integer nonlinear programs, and it has more agent-facing features than anything else we have built. It ships 17 expert subagents (for MINLP solving, convexity detection, Ipopt, benchmarking and more) and commands for each stage of modeling: formulating, diagnosing, debugging, reformulating, explaining a model, converting it between formats, and reporting on benchmarks. It also has a tool-call interface for building models from a natural-language description. The agent creates variables and adds constraints and logical conditions through tool calls, and the expressions it writes go through a safe parser instead of being executed as code. When a model is infeasible or a solve goes wrong, discopt produces reports on infeasibility, convexity and provenance that an agent can act on.</p>
<p>Around discopt is a growing set of <a href="https://kitchingroup.cheme.cmu.edu/software.html#discopt-plugins">plugins</a> that bring the same approach to particular domains. Each one checks its input against a schema, solves the problem, and reports the result, through a skill or an MCP server. discopt-mkm does microkinetic modeling for catalysis, including <a href="https://kitchingroup.cheme.cmu.edu/papers/yang2022b/index.html">degree of rate control</a> and limiting potentials. discopt-grid does power systems optimization, and it was built largely by an autonomous agent loop working from a written specification and implementation plan. discopt-doe adds design of experiments, with agents for estimability and identifiability analysis. discopt-flowsheet raises typed errors instead of returning silently wrong answers. discopt-course is an optimization course in which Claude Code is the teaching assistant, grading against rubrics. discopt-calphad, covered in the computing section, is one more plugin.</p>
</section>
<section id="where-this-is-going" class="level2">
<h2 class="anchored" data-anchor-id="where-this-is-going">Where this is going</h2>
<p>The direction is agents that run the whole loop, from literature to computation to experiment to paper, with people setting the goals and checking the results. The recent roadmap for AI in heterogeneous catalysis describes that future for one field <a href="https://kitchingroup.cheme.cmu.edu/papers/xin2026/index.html">(Xin et al., 2026)</a>. The software here is our attempt to build the environment it needs.</p>
<p>If I had to compress the approach into advice for students, it would be four things. Own the verification: you are responsible for every number, line of code and citation, so learn enough of the fundamentals to know when an answer is wrong. Write things down in plain text, where an agent can read them. Specify, then delegate: the skill that matters now is stating a problem precisely, with tests or checks that define when it is done. And be ambitious, and disclose. Attempt projects that were out of reach, and take responsibility for what you generate.</p>
<p>AI does not reduce the need for expertise. It makes judgment more valuable.</p>
<p>All of the software is on our <a href="https://kitchingroup.cheme.cmu.edu/software.html">software page</a> and at <a href="https://github.com/jkitchin">github.com/jkitchin</a>.</p>


</section>

 ]]></content:encoded></item><item><title>Surrogate models you can optimize globally</title><link>https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0000</pubDate><category>python</category><category>pycse</category><category>optimization</category><category>surrogates</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/</guid><description>Surrogate models you can optimize globally</description><content:encoded><![CDATA[ 

Skip to main content




<p>We often fit surrogate models so that we can use them in optimization problems. We want the conditions that maximize a predicted yield, the design that minimizes a predicted cost, or the next experiment to run. With a black-box model that usually means a local optimizer and some restarts, and at the end you have an answer with no way to know whether a better one exists somewhere you did not look. This is a well-known weak point of surrogate-based optimization <span class="citation" data-cites="bhosekar2018surrogate cozad2014alamo">(Bhosekar and Ierapetritou 2018; Cozad et al. 2014)</span>.</p>
<p>There is a class of models where you can do much better. Some surrogates are <em>piecewise linear</em> once they are trained: networks built from affine maps and ReLUs, Kolmogorov-Arnold networks with linear splines, regression trees with linear leaves, and sparse regressions on hinge functions. A piecewise-linear function can be written <em>exactly</em> as a set of mixed-integer linear constraints. The trained model then becomes part of a mixed-integer linear program (MILP), and a MILP solver returns the <strong>global</strong> optimum of the model together with a proof that nothing better exists inside the input box. Better still, the surrogate is now just one block in an optimization model, so you can add your own constraints, integer decisions and other models around it.</p>
<p>Over the past few months we have added this capability to four of our packages: <a href="https://github.com/jkitchin/pycse">pycse</a> (most of the <code>pycse.sklearn</code> models), <a href="https://github.com/jkitchin/jax-ldt">jax-ldt</a> (linear and hyperplane decision trees), <a href="https://github.com/jkitchin/jax-difftree">jax-difftree</a> (differentiable soft trees), and <a href="https://github.com/jkitchin/jaxsr">jaxsr</a> (symbolic regression). This post explains why you would want it, how the models get into a solver, and what each package offers, including the limitations.</p>
<p>The figure below shows the problem and the solution on a small example. The surrogate is a ReLU network with one input and 17 hidden units, so it is piecewise linear with one kink per unit, and it has four valleys. The deepest one is narrow. Press <em>5 random starts</em> to run local search (here, exact steepest descent along the curve): each start slides into the nearest valley and stops, and with these starts none of them finds the deepest one. Nothing in the result tells you that a better answer exists. Then <em>Step</em> through branch and bound. In the MILP, each ReLU has a binary variable, and in one dimension deciding a binary is the same as deciding which side of that unit’s kink <img src="https://latex.codecogs.com/png.latex?x"> lies on, so each branch splits the interval at a kink. On each piece, the units whose kink is outside the piece are fixed and the rest are relaxed, which gives a lower bound (the green segments). Any piece whose bound is above the best point found so far cannot contain the minimum, so it is discarded. When no pieces are left, the best point is proved to be the global minimum. Real MILP solvers bound with LP relaxations, which are tighter than the interval bounds used here, but the logic is the same.</p>
<iframe id="bnb-fig" src="https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/bnb.html" title="Local search versus branch and bound on a piecewise-linear surrogate" style="width:100%; height:720px; border:0;" loading="lazy"></iframe>
<script>
  window.addEventListener("message", e => {
    const h = e.data && e.data.mipBnbHeight;
    if (h) document.getElementById("bnb-fig").style.height = (h + 4) + "px";
  });
</script>
<section id="why-global-optimization-of-the-surrogate-matters" class="level2">
<h2 class="anchored" data-anchor-id="why-global-optimization-of-the-surrogate-matters">Why global optimization of the surrogate matters</h2>
<p>The usual way to optimize a neural network or a smooth tree is gradient-based: start somewhere, follow the gradient, restart a few times. That works when the surrogate is nearly convex. When the surrogate has several basins, which is common for exactly the response surfaces we care about (selectivity versus temperature and pressure, cost versus a handful of design variables) you have little control over which one you find. The jax-difftree docs say it plainly: inverse design over a soft tree is local, and multistart is the practical fallback.</p>
<p>A MIP solver works differently. It bounds the objective over regions of the input space and discards regions that cannot contain the optimum. When it finishes, the optimality gap is zero (or a tolerance you chose), and that is a statement about the whole box, not about the neighborhood of a starting point. For smooth networks you can get the same guarantee from deterministic global MINLP solvers <span class="citation" data-cites="schweidtmann2019global">(Schweidtmann and Mitsos 2019)</span>, but for piecewise-linear models the problem is a MILP, and MILP solvers are fast, mature and free (HiGHS <span class="citation" data-cites="huangfu2018highs">(Huangfu and Hall 2018)</span> solves every MILP in this post).</p>
<p>There is a second reason that matters just as much in practice. A surrogate embedded as constraints composes with everything else. You can put two unit-operation surrogates in one flowsheet, add a budget constraint, choose between discrete equipment options, and still get a global optimum of the combined problem. A <code>predict</code> function cannot do that.</p>
</section>
<section id="what-mip-representable-means" class="level2">
<h2 class="anchored" data-anchor-id="what-mip-representable-means">What “MIP-representable” means</h2>
<p>The building block is the ReLU, <img src="https://latex.codecogs.com/png.latex?y%20=%20%5Cmax(0,%20a)">. If you know bounds <img src="https://latex.codecogs.com/png.latex?L%20%5Cle%20a%20%5Cle%20U"> with <img src="https://latex.codecogs.com/png.latex?L%20%3C%200%20%3C%20U">, one binary variable <img src="https://latex.codecogs.com/png.latex?d"> encodes it exactly:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ay%20%5Cge%200,%20%5Cqquad%20y%20%5Cge%20a,%20%5Cqquad%20y%20%5Cle%20a%20-%20L(1%20-%20d),%20%5Cqquad%20y%20%5Cle%20U%20d.%0A"></p>
<p>When <img src="https://latex.codecogs.com/png.latex?d%20=%201"> the constraints force <img src="https://latex.codecogs.com/png.latex?y%20=%20a">, and when <img src="https://latex.codecogs.com/png.latex?d%20=%200"> they force <img src="https://latex.codecogs.com/png.latex?y%20=%200">. This is the big-M formulation, and it has been used for trained ReLU networks for some time <span class="citation" data-cites="fischetti2018deep tjeng2019evaluating grimstad2019relu">(Fischetti and Jo 2018; Tjeng et al. 2019; Grimstad and Andersson 2019)</span>. Stronger formulations exist <span class="citation" data-cites="anderson2020strong">(Anderson et al. 2020)</span>, and <span class="citation" data-cites="vielma2015mixed">Vielma (2015)</span> is a good general reference. The same idea covers hinge terms <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20x%20-%20k)">, absolute values, linear splines (with SOS2 or segment binaries), and tree routing (one binary per leaf).</p>
<p>Two consequences follow from that formulation and show up in every section below. First, <strong>the bounds <img src="https://latex.codecogs.com/png.latex?L"> and <img src="https://latex.codecogs.com/png.latex?U"> come from bounds on the inputs</strong>, propagated through the model. Every input must have finite bounds, and tight bounds give small big-M constants, which gives fast solves. Loose bounds are the most common cause of a slow MILP. Second, the formulation is exact only for piecewise-linear pieces. A SiLU, tanh or softplus activation, a cubic spline, or a polynomial term is not piecewise linear, and you either retrain with a piecewise-linear choice or move to a nonlinear solver.</p>
<p>It helps to name the levels of representability, since not every model lands in the same place.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 11%">
<col style="width: 42%">
<col style="width: 46%">
</colgroup>
<thead>
<tr class="header">
<th>Level</th>
<th>What it means</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Exact LP</td>
<td>No binaries needed</td>
<td>Linear models; input-convex networks when the output is minimized</td>
</tr>
<tr class="even">
<td>Exact MILP</td>
<td>Reproduces the model to solver precision</td>
<td>ReLU networks, linear-spline KANs, linear-leaf trees, hinge regressions</td>
</tr>
<tr class="odd">
<td>Approximate MILP</td>
<td>Some 1D nonlinearity interpolated on a grid, with an error bound</td>
<td>pycse’s periodic network (sin/cos features)</td>
</tr>
<tr class="even">
<td>Exact MINLP</td>
<td>The model is written as its own nonlinear equation</td>
<td>SISSO equations, jaxsr models with <code>allow_nonlinear=True</code></td>
</tr>
</tbody>
</table>
<p>Uncertainty adds one more ingredient. For several pycse models the predicted standard deviation is a second-order cone in the network’s internal variables, so robust objectives like <img src="https://latex.codecogs.com/png.latex?%5Cmu%20+%202%5Csigma"> become mixed-integer second-order cone programs. We come back to this in the pycse section.</p>
</section>
<section id="getting-a-surrogate-into-a-solver" class="level2">
<h2 class="anchored" data-anchor-id="getting-a-surrogate-into-a-solver">Getting a surrogate into a solver</h2>
<p>There are three routes from a trained model to an optimization problem, and our packages use all of them.</p>
<p><strong>Direct Pyomo embedding.</strong> The model writes its own variables and constraints into a <a href="https://www.pyomo.org/">Pyomo</a> <code>Block</code> <span class="citation" data-cites="bynum2021pyomo">(Bynum et al. 2021)</span>. You pass in your own Pyomo variables as inputs, and the block links them to the surrogate. This is the most flexible route and the one every package here supports (<code>to_pyomo</code> in pycse, jaxsr and jax-difftree, <code>embed_in_pyomo_model</code> in jax-ldt). Any Pyomo MILP solver works: HiGHS, Gurobi, CPLEX, SCIP.</p>
<p><strong>OMLT.</strong> <a href="https://github.com/cog-imperial/OMLT">OMLT</a> <span class="citation" data-cites="ceccon2022omlt">(Ceccon et al. 2022)</span> reads a neural network (usually from ONNX) and builds a Pyomo block with a choice of formulations, such as <code>ReluBigMFormulation</code>. OMLT also has tree formulations, including linear model decision trees <span class="citation" data-cites="ammari2023linear">(Ammari et al. 2023)</span>. In our packages, pycse’s <code>LLPRRegressor</code> exports to ONNX and directly to an OMLT <code>NetworkDefinition</code> (<code>to_onnx()</code>, <code>to_omlt()</code>). One caveat: jax-ldt also writes ONNX, but OMLT cannot read it. OMLT’s ONNX reader understands neural-network layers (Gemm, MatMul, ReLU), and the tree-routing operations (Gather, LessOrEqual) are rejected. That is why the tree packages ship their own adapters.</p>
<p><strong>discopt.</strong> <a href="https://github.com/jkitchin/discopt">discopt</a> is our hybrid MINLP solver with a JAX backend. jax-ldt (<code>embed_in_discopt_model</code>) and jaxsr (<code>to_discopt</code>) write directly into a <code>discopt.Model</code>, and jax-difftree reaches discopt through the jax-ldt spec (more on that below). The advantage of discopt is that the same model can hold MILP surrogate blocks and nonlinear constraints, which is where jaxsr’s <code>allow_nonlinear=True</code> and pycse’s SISSO export are headed. pycse itself writes Pyomo, but the ONNX file from <code>LLPRRegressor.to_onnx()</code> can also be read by <code>discopt.ml.load_onnx</code>.</p>
<p>Whatever the route, the pattern is the same: declare bounded input variables, embed the surrogate, add your constraints, set an objective on the surrogate output, solve, and then check the answer against the model’s own <code>predict</code>.</p>
</section>
<section id="pycse.sklearn-models" class="level2">
<h2 class="anchored" data-anchor-id="pycse.sklearn-models">pycse.sklearn models</h2>
<p>pycse has the broadest coverage. A new module in pycse 2.13.0, <code>pycse.sklearn.mip</code>, holds the formulation machinery once, and each estimator only describes its trained weights. A model implements <code>mip_network()</code>, which returns its weights as plain numpy layer specs (<code>Affine</code>, <code>ReLU</code>, <code>PiecewiseLinear</code>), a <code>TreeModel</code> of leaf boxes, or a symbolic expression. A single function, <code>add_to_pyomo</code>, turns any of these into constraints, so the bound propagation, the big-M formulation and the tests are shared across models. For the user it is one call:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> pyomo.environ <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> pyo</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> pycse.sklearn <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> DPOSE</span>
<span id="cb1-3"></span>
<span id="cb1-4">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> DPOSE(layers<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">32</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">16</span>)).fit(X, y)       <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ReLU by default</span></span>
<span id="cb1-5"></span>
<span id="cb1-6">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.ConcreteModel()</span>
<span id="cb1-7">m.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">500</span>))</span>
<span id="cb1-8">m.P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>))</span>
<span id="cb1-9">m.budget <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Constraint(expr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.T <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> m.P <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># your own constraints</span></span>
<span id="cb1-10">m.nn <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Block()</span>
<span id="cb1-11">model.to_pyomo(block<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.nn, inputs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[m.T, m.P])        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># input bounds come from your vars</span></span>
<span id="cb1-12">m.obj <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Objective(expr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.nn.y[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], sense<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>pyo.maximize)</span>
<span id="cb1-13">pyo.SolverFactory(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"highs"</span>).solve(m)</span></code></pre></div></div>
<p>Internal input and output scaling is folded into the exported weights, so <code>m.T</code>, <code>m.P</code> and <code>m.nn.y</code> are in your units.</p>
<section id="which-models-and-what-they-need" class="level3">
<h3 class="anchored" data-anchor-id="which-models-and-what-they-need">Which models, and what they need</h3>
<table class="caption-top table">
<colgroup>
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 20%">
</colgroup>
<thead>
<tr class="header">
<th>Model</th>
<th>Requirement</th>
<th>Level</th>
<th>Binaries</th>
<th>Std exported</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>DPOSE</code> <span class="citation" data-cites="kellner2024dpose">(Kellner and Ceriotti 2024)</span></td>
<td><code>activation=nn.relu</code> (default)</td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (ensemble)</td>
</tr>
<tr class="even">
<td><code>LLPRRegressor</code> <span class="citation" data-cites="bigi2024llpr">(Bigi et al. 2024)</span></td>
<td><code>activation="relu"</code></td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (LLPR)</td>
</tr>
<tr class="odd">
<td><code>NNBR</code></td>
<td><code>MLPRegressor(activation="relu")</code></td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (Bayesian ridge)</td>
</tr>
<tr class="even">
<td><code>KfoldNN</code></td>
<td><code>activation="relu"</code> (default is swish)</td>
<td>MILP</td>
<td>≤ 1 per ReLU unit</td>
<td>yes (ensemble)</td>
</tr>
<tr class="odd">
<td><code>JAXICNNRegressor</code> <span class="citation" data-cites="amos2017icnn">(Amos et al. 2017)</span></td>
<td><code>activation="relu"</code>, <code>strong_convexity_mu=0</code></td>
<td>MILP, or LP</td>
<td>0 with <code>formulation="convex"</code></td>
<td>no</td>
</tr>
<tr class="even">
<td><code>JAXMonotonicRegressor</code></td>
<td><code>activation="relu"</code></td>
<td>MILP; LP if <code>convexity="convex"</code></td>
<td>≤ 1 per ReLU unit</td>
<td>yes (LLPR)</td>
</tr>
<tr class="odd">
<td><code>KAN</code> <span class="citation" data-cites="liu2024kan">(Liu et al. 2024)</span></td>
<td><code>spline_order=1</code>, <code>base_activation="linear"</code></td>
<td>MILP</td>
<td>~<code>grid_size</code> per layer input</td>
<td>with <code>n_ensemble &gt; 1</code></td>
</tr>
<tr class="even">
<td><code>KANLLPR</code></td>
<td><code>spline_order=1</code>, <code>base_activation="linear"</code></td>
<td>MILP</td>
<td>~<code>grid_size</code> per layer input</td>
<td>yes (LLPR)</td>
</tr>
<tr class="odd">
<td><code>LeafModelRegressor</code></td>
<td>a linear <code>leaf_model</code></td>
<td>MILP</td>
<td>1 per leaf in the box</td>
<td>no</td>
</tr>
<tr class="even">
<td><code>LinearRegressionUQ</code></td>
<td>none</td>
<td>LP</td>
<td>0</td>
<td>yes</td>
</tr>
<tr class="odd">
<td><code>JAXPeriodicRegressor</code></td>
<td><code>activation="relu"</code></td>
<td>approximate MILP</td>
<td>grid segments + ReLUs</td>
<td>yes</td>
</tr>
<tr class="even">
<td><code>SISSO</code> <span class="citation" data-cites="ouyang2018sisso">(Ouyang et al. 2018)</span></td>
<td>none (TorchSisso to fit)</td>
<td>MINLP</td>
<td>0, nonlinear</td>
<td>yes</td>
</tr>
</tbody>
</table>
<p>“One binary per ReLU unit” is an upper bound. Units that are always on or always off over the input box need no binary, and interval bound propagation finds them. A DPOSE network with 32 hidden units needed 30 binaries in the docs notebook.</p>
<p>A few of the formulations are worth a sentence each. KAN layers apply a 1D spline to every edge, but all the edges leaving one input share its breakpoints, so the MIP uses one SOS2 set per <em>input</em> rather than per edge. Trees use one binary to pick a leaf, and leaves that lie outside the input bounds are dropped before the MIP is built. Leaf models are recovered by probing <code>predict</code> and then checked at random points, so any linear scikit-learn pipeline works and a nonlinear one raises an error.</p>
<p>Input-convex networks get special treatment. In an input-convex ReLU network every hidden unit reaches the output through nonnegative weights, so when the output is <em>minimized</em>, the two lower inequalities <img src="https://latex.codecogs.com/png.latex?y%20%5Cge%200,%20y%20%5Cge%20a"> are enough: the solver pushes each unit down to <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20a)"> on its own. <code>formulation="convex"</code> drops all the binaries and the problem becomes an LP. pycse checks the weights first and refuses networks that are not convex.</p>
</section>
<section id="how-it-performed" class="level3">
<h3 class="anchored" data-anchor-id="how-it-performed">How it performed</h3>
<p>To test the exports we fitted six models to the same 400 noisy samples of <img src="https://latex.codecogs.com/png.latex?f(x)%20=%20%5Ctfrac14%5ClVert%20x%5CrVert%5E2%20+%20%5Csin%203x_1%20+%20%5Ccos%202x_2%20+%200.15x_2"> on the square <img src="https://latex.codecogs.com/png.latex?%5B-2,%202%5D%5E2">, then minimized each model with HiGHS. The function has one global minimum, <img src="https://latex.codecogs.com/png.latex?f%5E*%20=%20-1.599"> at <img src="https://latex.codecogs.com/png.latex?(-0.50,%20-1.43)">, and several local ones. The deepest of these is a mirror-image well at <img src="https://latex.codecogs.com/png.latex?(-0.50,%201.36)"> with <img src="https://latex.codecogs.com/png.latex?f%20=%20-1.181">, the obvious place for a model to go wrong. The results answer three questions. (They come from one run. Different training runs shift the numbers a little, but not the conclusions.)</p>
<p><strong>Is the MIP exact?</strong> Yes. For every model, the MIP minimum was equal to or slightly below the minimum of a 301×301 grid search on the same model, which is what a true global minimum must do. Evaluating the model with its own <code>predict</code> at the MIP’s answer reproduced the MIP objective to <img src="https://latex.codecogs.com/png.latex?10%5E%7B-11%7D"> or better.</p>
<p><strong>Is the answer right?</strong> Yes, when the model is good. These fits have test RMSE between 0.07 and 0.11, and every one of their MIP optima landed in the global well, within 0.14 of <img src="https://latex.codecogs.com/png.latex?f%5E*"> when judged by the true function. None of them fell into the second well. The figure below shows where each one landed. A model has to be good enough for this. With 300 training epochs <code>LLPRRegressor</code> underfit (RMSE 0.24), and its optimum missed <img src="https://latex.codecogs.com/png.latex?f%5E*"> by 0.5, so we trained it for 1000. All six models predict a minimum slightly <em>deeper</em> than the true one (between <img src="https://latex.codecogs.com/png.latex?-1.69"> and <img src="https://latex.codecogs.com/png.latex?-1.83">). That is expected: a global optimizer seeks out the places where the model’s error happens to be most negative, so the predicted value at the optimum is biased low. Check the optimum against the real function before you trust the number.</p>
<p><strong>What does it cost?</strong> The number of binaries grows with the size of the network, and the solve time grows faster than that:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Model</th>
<th>Binaries</th>
<th>Solve (s)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>KfoldNN (32, 16)</td>
<td>29</td>
<td>0.06</td>
</tr>
<tr class="even">
<td>DPOSE (32 hidden units)</td>
<td>29</td>
<td>0.10</td>
</tr>
<tr class="odd">
<td>KAN (2, 4, 1), grid 10</td>
<td>68</td>
<td>0.29</td>
</tr>
<tr class="even">
<td>KANLLPR (2, 4, 1), grid 10</td>
<td>66</td>
<td>0.55</td>
</tr>
<tr class="odd">
<td>LLPRRegressor (32, 32)</td>
<td>63</td>
<td>8.1</td>
</tr>
<tr class="even">
<td>NNBR (32, 32)</td>
<td>59</td>
<td>9.3</td>
</tr>
</tbody>
</table>
<p>The two networks with two hidden layers of 32 units are the slow ones. KANs have about as many binaries but solve much faster, because their binaries come from splines on the inputs rather than from stacked ReLU layers. The convex formulation is cheaper still: on a convex test function, <code>JAXICNNRegressor</code> (64, 64) took 0.47 s with 122 binaries as a MILP and 0.06 s with no binaries as an LP, and both found the same minimum.</p>
<p>We left the tree (<code>LeafModelRegressor</code>) out of this comparison. On a smooth function like this one, 400 points are not enough for a tree to fit well (test RMSE 0.29 or more), and its optimum missed <img src="https://latex.codecogs.com/png.latex?f%5E*"> by 0.9 to 2.7 depending on the settings. The MIP still finds the tree’s minimum exactly; the tree is just a poor model of <img src="https://latex.codecogs.com/png.latex?f"> here.</p>
<p>Hover a point or its label in the figure to see which model it belongs to, and click it to show that model’s surface.</p>
<iframe id="optima-fig" src="https://kitchingroup.cheme.cmu.edu/posts/mip-surrogates/optima.html?v=2" title="Where each model puts the minimum" style="width:100%; height:640px; border:0;" loading="lazy"></iframe>
<script>
  window.addEventListener("message", e => {
    const h = e.data && e.data.mipOptimaHeight;
    if (h) document.getElementById("optima-fig").style.height = (h + 4) + "px";
  });
</script>
</section>
<section id="optimizing-with-uncertainty" class="level3">
<h3 class="anchored" data-anchor-id="optimizing-with-uncertainty">Optimizing with uncertainty</h3>
<p>For every pycse model with a standard deviation, the std has the form <img src="https://latex.codecogs.com/png.latex?%5Csigma(x)%20=%20%5Csqrt%7B%5ClVert%20A%20z(x)%20+%20c%20%5CrVert%5E2%20+%20d%7D">, where <img src="https://latex.codecogs.com/png.latex?z"> is one layer of the network (the ensemble members for DPOSE and KfoldNN, the last-layer features for the LLPR models). That is a second-order cone. <code>to_pyomo(include_std=True)</code> adds a variable with <img src="https://latex.codecogs.com/png.latex?s%20%5Cge%20%5Csigma(x)">, which is convex and exact whenever the objective <em>penalizes</em> the std, as in robust design (<img src="https://latex.codecogs.com/png.latex?%5Cmin%5C%20%5Cmu%20+%202%5Csigma">). <code>include_std="exact"</code> enforces <img src="https://latex.codecogs.com/png.latex?s%5E2%20=%20%5Csigma(x)%5E2"> instead. It is nonconvex, and it is needed when the objective <em>rewards</em> the std, as in exploration for active learning (<img src="https://latex.codecogs.com/png.latex?%5Cmin%5C%20%5Cmu%20-%202%5Csigma">), because with the cone alone that problem is unbounded.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Objective</th>
<th>Mean at x*</th>
<th>Std at x*</th>
<th>MIP objective</th>
<th>Grid</th>
<th>Solve</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>mean</td>
<td>-1.7826</td>
<td>0.1024</td>
<td>-1.7826</td>
<td>-1.7800</td>
<td>0.36 s</td>
</tr>
<tr class="even">
<td>robust: mean + 2 std</td>
<td>-1.7693</td>
<td>0.0932</td>
<td>-1.5830</td>
<td>-1.5792</td>
<td>0.21 s</td>
</tr>
<tr class="odd">
<td>explore: mean - 2 std</td>
<td>-1.7769</td>
<td>0.1076</td>
<td>-1.9923</td>
<td>-1.9900</td>
<td>4.2 s</td>
</tr>
</tbody>
</table>
<p>Penalizing the std trades a little mean for less uncertainty, and rewarding it does the opposite. Both std options need a solver with quadratic constraints (Gurobi, CPLEX, SCIP). These numbers are a separate DPOSE fit from the docs notebook, solved with Gurobi; <code>pip install gurobipy</code> includes a size-limited license that covers small examples like this.</p>
</section>
<section id="caveats" class="level3">
<h3 class="anchored" data-anchor-id="caveats">Caveats</h3>
<p>You have to choose piecewise-linear options at training time. Several pycse defaults are smooth (LLPR, ICNN, monotonic, periodic and KfoldNN, plus KAN’s cubic splines), and <code>to_pyomo()</code> raises a <code>ValueError</code> naming the setting to change. The accuracy cost of ReLU or linear splines is usually small, but it is a retraining step.</p>
<p>MIP size grows with the network. Two hidden layers of 32 units solved in 7 to 15 s here, and much larger networks may not solve in reasonable time. KANs (binaries per input, not per unit) and the convex LP scale better.</p>
<p>The convex LP is one-sided: it is exact only when the output is minimized or bounded above. To maximize an ICNN, use the MILP.</p>
<p>Periodic models are approximate. The sin/cos features are interpolated on a grid, with a per-feature error of at most <img src="https://latex.codecogs.com/png.latex?(2%5Cpi/n)%5E2/8"> for <img src="https://latex.codecogs.com/png.latex?n"> points per period (4.8e-3 at the default of 32). The MIP finds the global optimum of the <em>interpolated</em> model, which in the notebook landed 2.4e-3 above the grid-search minimum of the un-interpolated model at 8 to 32 points. At 64 points it beat the grid search, at the cost of 288 binaries instead of 160.</p>
<p>Trees have a boundary problem. A tree sends <img src="https://latex.codecogs.com/png.latex?x%20%5Cle%20t"> left, and a MIP cannot express the strict inequality on the other side, so a point exactly on a split can take either leaf’s value. <code>split_gap=1e-4</code> excludes a thin slab above each split. The gap has to be well above the solver tolerances; at 1e-6 HiGHS still picked the wrong leaf.</p>
<p>SISSO exports exactly, but the equation is nonlinear (division, exp, log), so it needs a global MINLP solver such as BARON, SCIP or Gurobi’s nonlinear mode. Ipopt will only find a local optimum.</p>
<p><code>ZENNRegressor</code> and <code>ZENNClassifier</code> are not representable (their output is a log-sum-exp over sub-networks). <code>NNGMM</code>’s mean network could be exported but is not wired up yet, and <code>JAXICNNRegressor</code> with <code>strong_convexity_mu &gt; 0</code> would be a MIQP but is not exported.</p>
<p>The walkthrough is the pycse docs page <a href="https://kitchingroup.cheme.cmu.edu/pycse/docs/mip.html">Optimizing over machine-learned models with MIP</a>. The code is in <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/sklearn/mip.py"><code>mip.py</code></a> (its module docstring has a worked example), with tests in <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/tests/test_sklearn_mip.py"><code>test_sklearn_mip.py</code></a>, and a KAN versus ReLU size comparison in <a href="https://github.com/jkitchin/pycse/blob/master/src/pycse/examples/KAN_Demo.ipynb"><code>KAN_Demo.ipynb</code></a>.</p>
</section>
</section>
<section id="jax-ldt-linear-and-hyperplane-decision-trees" class="level2">
<h2 class="anchored" data-anchor-id="jax-ldt-linear-and-hyperplane-decision-trees">jax-ldt: linear and hyperplane decision trees</h2>
<p><a href="https://github.com/jkitchin/jax-ldt">jax-ldt</a> fits linear-model decision trees, where every leaf holds a linear regression instead of a constant, and hyperplane trees, where the splits are oblique hyperplanes chosen from sparse combinations of the inputs. Both are piecewise linear by construction, so they were designed from the start to be embedded. The package was motivated by our work with the Laird group on hyperplane trees as surrogates for chemical process design <span class="citation" data-cites="sunshine2025hyperplane">(Sunshine et al. 2025)</span>, which builds on linear model decision trees in OMLT <span class="citation" data-cites="ammari2023linear">(Ammari et al. 2023)</span>. Optimization over tree models more generally goes back to <span class="citation" data-cites="misic2020optimization">Mišić (2020)</span> and ENTMOOT <span class="citation" data-cites="thebelt2021entmoot">(Thebelt et al. 2021)</span>.</p>
<p>The MILP formulation lifts the features into the transformed (hyperplane) space, uses one binary per leaf, writes the routing as big-M constraints, and linearizes the leaf outputs with big-M as well. The result is an exact MILP. The same formulation is available in Pyomo and in discopt:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> discopt</span>
<span id="cb2-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> jax_ldt <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> HyperplaneTreeRegressor</span>
<span id="cb2-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> jax_ldt.export <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> embed_in_discopt_model</span>
<span id="cb2-4"></span>
<span id="cb2-5">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> HyperplaneTreeRegressor(max_depth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>, max_bins<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, min_samples_leaf<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">15</span>)</span>
<span id="cb2-6">model.fit(X, y)</span>
<span id="cb2-7"></span>
<span id="cb2-8">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> discopt.Model(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"min-y"</span>)</span>
<span id="cb2-9">x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> m.continuous(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x"</span>, shape<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,), lb<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, ub<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb2-10">y_expr <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> embed_in_discopt_model(model, m, x)</span>
<span id="cb2-11">m.minimize(y_expr[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>])</span>
<span id="cb2-12">result <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> m.solve()</span></code></pre></div></div>
<p>For Pyomo, <code>embed_in_pyomo_model(model, m, m.x)</code> builds the same formulation on a sub-block, so several trees can share one model. For axis-aligned trees with constant leaves there is also a lighter adapter, <code>to_discopt_decision_tree</code>, which uses discopt’s built-in tree formulation.</p>
<p>The canonical export is a JSON spec (<code>to_json</code> / <code>from_json</code>), and every other format is regenerated from it. That turns out to be useful beyond jax-ldt, because jax-difftree writes the same spec.</p>
<p><strong>Caveats.</strong> Big-M constants are derived per node and per leaf from the input bounds, so bounds are required. You can override them with <code>big_m=</code>, and they are validated when the inputs are bounded. The ONNX export is for inference with onnxruntime, not for OMLT, as noted above. The tree’s uncertainty estimates are stored in the spec but are not part of the MILP.</p>
<p>Docs: <a href="https://github.com/jkitchin/jax-ldt/blob/main/docs/mip_export.md">MIP export</a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/docs/discopt_integration.md">discopt integration</a>. Examples: <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/04_mip_optimization.ipynb"><code>04_mip_optimization.ipynb</code></a>, <a href="https://github.com/jkitchin/jax-ldt/blob/main/examples/07_pyomo_embedding.ipynb"><code>07_pyomo_embedding.ipynb</code></a>.</p>
</section>
<section id="jax-difftree-differentiable-soft-trees" class="level2">
<h2 class="anchored" data-anchor-id="jax-difftree-differentiable-soft-trees">jax-difftree: differentiable soft trees</h2>
<p><a href="https://github.com/jkitchin/jax-difftree">jax-difftree</a> takes a different route to a tree. Instead of growing it greedily, it fits a fixed-depth <em>soft</em> tree end to end with gradients <span class="citation" data-cites="hehn2020end">(Hehn et al. 2020)</span>, with oblique splits and sigmoid routing. Soft trees are smooth and train well, and they can match derivative data (Jacobians from a simulator) as well as values. They are not piecewise linear, though, so getting a MILP takes one extra step.</p>
<p>The workflow is to train with affine leaves, harden the tree so that each point routes to exactly one leaf, and then export the crisp piecewise-affine model:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> difftree <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> DifferentiableTreeRegressor, export</span>
<span id="cb3-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> pyomo.environ <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> pyo</span>
<span id="cb3-3"></span>
<span id="cb3-4">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> DifferentiableTreeRegressor(</span>
<span id="cb3-5">    depth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>, leaf_type<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"affine"</span>, jacobian_weight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, hardening<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb3-6">)</span>
<span id="cb3-7">model.fit(X_tr, Y_tr, dydx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>J_tr)</span>
<span id="cb3-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(model.hardening_report_)            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># soft vs hard error, before and after</span></span>
<span id="cb3-9"></span>
<span id="cb3-10">m <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.ConcreteModel()</span>
<span id="cb3-11">m.x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb3-12">m.y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Var(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb3-13">tree <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> export.to_pyomo(model, m, x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.x, y<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.y, bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(lo, hi), method<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bigm"</span>)</span>
<span id="cb3-14">m.obj <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pyo.Objective(expr<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.y[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>])</span>
<span id="cb3-15">pyo.SolverFactory(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"appsi_highs"</span>).solve(m)</span></code></pre></div></div>
<p>The Pyomo export is written as a generalized disjunctive program: each leaf is a <code>Disjunct</code> holding its polytope and its affine model, and a <code>Disjunction</code> makes exactly one leaf active, which Pyomo then transforms (here with big-M). For discopt, <code>export.to_json</code> writes the jax-ldt v1 spec, so jax-ldt’s <code>from_json</code> and <code>embed_in_discopt_model</code> consume a difftree model unchanged. All outputs share one partition, which means a multi-output tree for a unit operation needs only one set of binaries.</p>
<p><strong>Caveats.</strong> The <em>soft/hard gap</em> is the main one. The MILP optimizes the hardened tree, not the soft one you trained, so check <code>soft_hard_gap</code> (and train with <code>hardening=True</code>) before trusting the optimum. The leaf polytopes are closed, as usual in big-M formulations, so an optimum can sit on a split boundary using either neighbor’s model; <code>export.repair_solution</code> moves such a point into the chosen leaf. Continuity across oblique splits needs <code>tied_splits=True</code>, which gives up flexibility (a depth-<img src="https://latex.codecogs.com/png.latex?D"> tree then has <img src="https://latex.codecogs.com/png.latex?D"> hyperplanes instead of <img src="https://latex.codecogs.com/png.latex?2%5ED%20-%201">). The fixed full-binary structure caps practical depth at around 10 to 12. A pruned export matches <code>predict(hard=True)</code> only inside the export box.</p>
<p>Docs: <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/surrogates.md">surrogates for MILP</a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/docs/limitations.md">limitations</a>. Notebooks: <a href="https://github.com/jkitchin/jax-difftree/blob/main/notebooks/piecewise_linear_milp.ipynb"><code>piecewise_linear_milp</code></a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/notebooks/pyomo_embedding.ipynb"><code>pyomo_embedding</code></a>, <a href="https://github.com/jkitchin/jax-difftree/blob/main/notebooks/multivariate_inverse_design.ipynb"><code>multivariate_inverse_design</code></a>.</p>
</section>
<section id="jaxsr-symbolic-regression-with-hinge-functions" class="level2">
<h2 class="anchored" data-anchor-id="jaxsr-symbolic-regression-with-hinge-functions">jaxsr: symbolic regression with hinge functions</h2>
<p><a href="https://github.com/jkitchin/jaxsr">jaxsr</a> fits sparse symbolic models, a weighted sum of terms chosen from a basis library by an information criterion, in the spirit of ALAMO <span class="citation" data-cites="cozad2014alamo">(Cozad et al. 2014)</span>. The new capability is a family of piecewise-linear basis terms, most usefully hinges <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20x%20-%20k)"> and <img src="https://latex.codecogs.com/png.latex?%5Cmax(0,%20k%20-%20x)">, which give MARS-style fits <span class="citation" data-cites="friedman1991mars breiman1993hinging">(Friedman 1991; Breiman 1993)</span>. If every selected term is piecewise linear, the fitted model is an exact MILP, and unlike the other models in this post it is also a short, readable equation.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> jaxsr <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> BasisLibrary, SymbolicRegressor, milp_incompatible_terms</span>
<span id="cb4-2"></span>
<span id="cb4-3">library <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb4-4">    BasisLibrary(n_features<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, feature_names<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"T"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"P"</span>], feature_bounds<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>), (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>)])</span>
<span id="cb4-5">    .add_constant()</span>
<span id="cb4-6">    .add_linear()</span>
<span id="cb4-7">    .add_hinges(X<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>X, n_knots<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span>)</span>
<span id="cb4-8">    .add_transcendental([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"abs"</span>])</span>
<span id="cb4-9">)</span>
<span id="cb4-10">model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> SymbolicRegressor(basis_library<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>library, max_terms<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>, information_criterion<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bic"</span>)</span>
<span id="cb4-11">model.fit(X, cost)</span>
<span id="cb4-12"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(model.expression_)</span>
<span id="cb4-13"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(milp_incompatible_terms(model))   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># [] means a pure MILP</span></span></code></pre></div></div>
<p>The MILP-representable terms are constants and linear terms (written directly), hinges and <code>abs</code> (one binary each, big-M), categorical indicators (one-hot binaries), and categorical-continuous interactions (an exact McCormick product). The model then goes to either backend with one call, linked to your own variables:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">model.to_pyomo(m.sr, inputs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"T"</span>: m.T, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"P"</span>: m.P}, output<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>m.cost)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pyomo block</span></span>
<span id="cb5-2"></span>
<span id="cb5-3">emb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> model.to_discopt(dm_model)                                    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># discopt</span></span>
<span id="cb5-4">dm_model.subject_to(emb.inputs[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"T"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> emb.inputs[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"P"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>)</span>
<span id="cb5-5">dm_model.minimize(emb.output)</span></code></pre></div></div>
<p>Polynomials, interactions, <code>log</code>, <code>exp</code> and the rest are not piecewise linear. You can still embed them with <code>allow_nonlinear=True</code>, which gives an MIQCP or MINLP for discopt, BARON, SCIP or Gurobi.</p>
<p><strong>Caveats.</strong> Bounds are required and are <em>enforced</em> on the embedded inputs, on purpose: outside the training range the model is extrapolating, and the right fix is more data, not wider bounds. Knot placement matters more than knot count, since the fit can only bend where a knot exists, so pass known breakpoints (a phase boundary, a saturation point) explicitly with <code>knots=</code>. Parametric and custom basis functions, rational forms and SISSO-style features cannot be embedded. <code>MultiOutputSymbolicRegressor</code> is not supported directly, but each <code>estimators_[i]</code> can be embedded separately. The embedding is built from the fitted coefficients, so rebuild it after a refit.</p>
<p>Guide: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/guides/mip-embedding.md">Embedding models in MILPs</a>. Example: <a href="https://github.com/jkitchin/jaxsr/blob/main/docs/examples/mip_surrogate_optimization.ipynb"><code>mip_surrogate_optimization.ipynb</code></a>, which includes a refine-and-resolve loop and a design problem solved in both backends.</p>
</section>
<section id="choosing-a-model" class="level2">
<h2 class="anchored" data-anchor-id="choosing-a-model">Choosing a model</h2>
<!-- TODO: optionally run one shared benchmark through all four packages for a fair
comparison of fit error, MILP size and solve time. -->
<p>The packages overlap, but they are good at different things. If you want an interpretable equation, or you know where the response bends, jaxsr’s hinge models are the natural choice, and they stay small. If you want a flexible fit with uncertainty and the option of a robust or exploratory objective, the pycse ReLU networks are the most complete, and the input-convex networks are the cheapest to optimize when convexity is physically justified. Trees are attractive when the response has genuinely different regimes: jax-ldt grows them greedily and exports directly, while jax-difftree trains them end to end, can use derivative data, and needs the hardening step before export.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
<col style="width: 16%">
</colgroup>
<thead>
<tr class="header">
<th></th>
<th>pycse ReLU nets</th>
<th>pycse KAN</th>
<th>jax-ldt</th>
<th>jax-difftree</th>
<th>jaxsr hinges</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Binaries scale with</td>
<td>hidden units</td>
<td>grid × inputs</td>
<td>leaves</td>
<td>leaves (<img src="https://latex.codecogs.com/png.latex?2%5ED">)</td>
<td>hinge/abs terms</td>
</tr>
<tr class="even">
<td>Interpretable</td>
<td>no</td>
<td>partly</td>
<td>partly</td>
<td>partly</td>
<td>yes</td>
</tr>
<tr class="odd">
<td>Std in the MIP</td>
<td>yes (SOCP)</td>
<td>with LLPR/ensemble</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr>
<tr class="even">
<td>Pyomo</td>
<td>yes</td>
<td>yes</td>
<td>yes</td>
<td>yes (GDP)</td>
<td>yes</td>
</tr>
<tr class="odd">
<td>discopt</td>
<td>LLPR via ONNX</td>
<td>no</td>
<td>yes</td>
<td>via jax-ldt spec</td>
<td>yes</td>
</tr>
<tr class="even">
<td>OMLT</td>
<td>LLPR via ONNX</td>
<td>no</td>
<td>no</td>
<td>no</td>
<td>no</td>
</tr>
</tbody>
</table>
</section>
<section id="the-caveat-that-applies-to-all-of-them" class="level2">
<h2 class="anchored" data-anchor-id="the-caveat-that-applies-to-all-of-them">The caveat that applies to all of them</h2>
<p>A global optimizer finds your model’s mistakes. Our first run of the pycse benchmark used 150 samples and shorter training. Two of the models came out poorly fitted, and their global optima sat on the edge of the box, where there was little data and the models were extrapolating: one predicted <img src="https://latex.codecogs.com/png.latex?-1.58"> at a point where the true <img src="https://latex.codecogs.com/png.latex?f"> is <img src="https://latex.codecogs.com/png.latex?+1.07">. A local optimizer started in the middle might never go there, and the MIP always will. The global optimum of the <em>surrogate</em> is only as good as the surrogate is in that region. So validate the optimum against the true model, and then either add data there and refit (jaxsr’s notebook has a refine-and-resolve loop, and our earlier <a href="https://kitchingroup.cheme.cmu.edu/posts/surrogate-active-learning/">active-learning post</a> covers the data side), penalize the std, or tighten the bounds to the region you trust.</p>
</section>
<section id="installing" class="level2">
<h2 class="anchored" data-anchor-id="installing">Installing</h2>
<p>The MIP export is in pycse 2.13.0 and later. Pyomo and HiGHS come with the <code>mip</code> extra; the ONNX and OMLT exports of <code>LLPRRegressor</code> also need <code>onnx</code> and <code>omlt</code>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pycse[mip]"</span></span></code></pre></div></div>
<p>jaxsr 0.3.0 is on PyPI, but the hinge terms and MILP embedding are newer than that release, so install from GitHub for now. jax-ldt and jax-difftree are not on PyPI. (The <code>difftree</code> package on PyPI is an unrelated project, so install jax-difftree from GitHub as shown.)</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jaxsr[mip] @ git+https://github.com/jkitchin/jaxsr.git"</span></span>
<span id="cb7-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jax-ldt[pyomo] @ git+https://github.com/jkitchin/jax-ldt.git"</span></span>
<span id="cb7-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"difftree[pyomo] @ git+https://github.com/jkitchin/jax-difftree.git"</span></span>
<span id="cb7-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">pip</span> install discopt omlt onnx</span></code></pre></div></div>
</section>
<section id="references" class="level2">
<h2 class="anchored" data-anchor-id="references">References</h2>
<div id="refs" class="references csl-bib-body hanging-indent">
<div id="ref-ammari2023linear" class="csl-entry">
Ammari, Bashar L., Emma S. Johnson, Georgia Stinchfield, et al. 2023. <span>“Linear Model Decision Trees as Surrogates in Optimization of Engineering Applications.”</span> <em>Computers &amp; Chemical Engineering</em> 178: 108347. <a href="https://doi.org/10.1016/j.compchemeng.2023.108347">https://doi.org/10.1016/j.compchemeng.2023.108347</a>.
</div>
<div id="ref-amos2017icnn" class="csl-entry">
Amos, Brandon, Lei Xu, and J. Zico Kolter. 2017. <span>“Input Convex Neural Networks.”</span> <em>Proceedings of the 34th International Conference on Machine Learning</em>. <a href="https://arxiv.org/abs/1609.07152">https://arxiv.org/abs/1609.07152</a>.
</div>
<div id="ref-anderson2020strong" class="csl-entry">
Anderson, Ross, Joey Huchette, Will Ma, Christian Tjandraatmadja, and Juan Pablo Vielma. 2020. <span>“Strong Mixed-Integer Programming Formulations for Trained Neural Networks.”</span> <em>Mathematical Programming</em> 183: 3–39. <a href="https://doi.org/10.1007/s10107-020-01474-5">https://doi.org/10.1007/s10107-020-01474-5</a>.
</div>
<div id="ref-bhosekar2018surrogate" class="csl-entry">
Bhosekar, Atharv, and Marianthi Ierapetritou. 2018. <span>“Advances in Surrogate Based Modeling, Feasibility Analysis, and Optimization: A Review.”</span> <em>Computers &amp; Chemical Engineering</em> 108: 250–67. <a href="https://doi.org/10.1016/j.compchemeng.2017.09.017">https://doi.org/10.1016/j.compchemeng.2017.09.017</a>.
</div>
<div id="ref-bigi2024llpr" class="csl-entry">
Bigi, Filippo, Sanggyu Chong, Michele Ceriotti, and Federico Grasselli. 2024. <span>“A Prediction Rigidity Formalism for Low-Cost Uncertainties in Trained Neural Networks.”</span> <em>Machine Learning: Science and Technology</em> 5 (4): 045018. <a href="https://doi.org/10.1088/2632-2153/ad805f">https://doi.org/10.1088/2632-2153/ad805f</a>.
</div>
<div id="ref-breiman1993hinging" class="csl-entry">
Breiman, Leo. 1993. <span>“Hinging Hyperplanes for Regression, Classification, and Function Approximation.”</span> <em>IEEE Transactions on Information Theory</em> 39 (3): 999–1013. <a href="https://doi.org/10.1109/18.256506">https://doi.org/10.1109/18.256506</a>.
</div>
<div id="ref-bynum2021pyomo" class="csl-entry">
Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, et al. 2021. <em>Pyomo: Optimization Modeling in Python</em>. 3rd ed. Vol. 67. Springer Optimization and Its Applications. Springer. <a href="https://doi.org/10.1007/978-3-030-68928-5">https://doi.org/10.1007/978-3-030-68928-5</a>.
</div>
<div id="ref-ceccon2022omlt" class="csl-entry">
Ceccon, Francesco, Jordan Jalving, Joshua Haddad, et al. 2022. <span>“<span>OMLT</span>: Optimization &amp; Machine Learning Toolkit.”</span> <em>Journal of Machine Learning Research</em> 23 (349): 1–8. <a href="https://jmlr.org/papers/v23/22-0277.html">https://jmlr.org/papers/v23/22-0277.html</a>.
</div>
<div id="ref-cozad2014alamo" class="csl-entry">
Cozad, Alison, Nikolaos V. Sahinidis, and David C. Miller. 2014. <span>“Learning Surrogate Models for Simulation-Based Optimization.”</span> <em>AIChE Journal</em> 60 (6): 2211–27. <a href="https://doi.org/10.1002/aic.14418">https://doi.org/10.1002/aic.14418</a>.
</div>
<div id="ref-fischetti2018deep" class="csl-entry">
Fischetti, Matteo, and Jason Jo. 2018. <span>“Deep Neural Networks and Mixed Integer Linear Optimization.”</span> <em>Constraints</em> 23: 296–309. <a href="https://doi.org/10.1007/s10601-018-9285-6">https://doi.org/10.1007/s10601-018-9285-6</a>.
</div>
<div id="ref-friedman1991mars" class="csl-entry">
Friedman, Jerome H. 1991. <span>“Multivariate Adaptive Regression Splines.”</span> <em>The Annals of Statistics</em> 19 (1): 1–67. <a href="https://doi.org/10.1214/aos/1176347963">https://doi.org/10.1214/aos/1176347963</a>.
</div>
<div id="ref-grimstad2019relu" class="csl-entry">
Grimstad, Bjarne, and Henrik Andersson. 2019. <span>“<span>ReLU</span> Networks as Surrogate Models in Mixed-Integer Linear Programs.”</span> <em>Computers &amp; Chemical Engineering</em> 131: 106580. <a href="https://doi.org/10.1016/j.compchemeng.2019.106580">https://doi.org/10.1016/j.compchemeng.2019.106580</a>.
</div>
<div id="ref-hehn2020end" class="csl-entry">
Hehn, Thomas M., Julian F. P. Kooij, and Fred A. Hamprecht. 2020. <span>“End-to-End Learning of Decision Trees and Forests.”</span> <em>International Journal of Computer Vision</em> 128 (4): 997–1011. <a href="https://doi.org/10.1007/s11263-019-01237-6">https://doi.org/10.1007/s11263-019-01237-6</a>.
</div>
<div id="ref-huangfu2018highs" class="csl-entry">
Huangfu, Qi, and J. A. Julian Hall. 2018. <span>“Parallelizing the Dual Revised Simplex Method.”</span> <em>Mathematical Programming Computation</em> 10: 119–42. <a href="https://doi.org/10.1007/s12532-017-0130-5">https://doi.org/10.1007/s12532-017-0130-5</a>.
</div>
<div id="ref-kellner2024dpose" class="csl-entry">
Kellner, Matthias, and Michele Ceriotti. 2024. <span>“Uncertainty Quantification by Direct Propagation of Shallow Ensembles.”</span> <em>Machine Learning: Science and Technology</em> 5 (3): 035006. <a href="https://doi.org/10.1088/2632-2153/ad594a">https://doi.org/10.1088/2632-2153/ad594a</a>.
</div>
<div id="ref-liu2024kan" class="csl-entry">
Liu, Ziming, Yixuan Wang, Sachin Vaidya, et al. 2024. <span>“<span>KAN</span>: Kolmogorov-Arnold Networks.”</span> <em>arXiv Preprint arXiv:2404.19756</em>, ahead of print. <a href="https://doi.org/10.48550/arXiv.2404.19756">https://doi.org/10.48550/arXiv.2404.19756</a>.
</div>
<div id="ref-misic2020optimization" class="csl-entry">
Mišić, Velibor V. 2020. <span>“Optimization of Tree Ensembles.”</span> <em>Operations Research</em> 68 (5): 1605–24. <a href="https://doi.org/10.1287/opre.2019.1928">https://doi.org/10.1287/opre.2019.1928</a>.
</div>
<div id="ref-ouyang2018sisso" class="csl-entry">
Ouyang, Runhai, Stefano Curtarolo, Emre Ahmetcik, Matthias Scheffler, and Luca M. Ghiringhelli. 2018. <span>“<span>SISSO</span>: A Compressed-Sensing Method for Identifying the Best Low-Dimensional Descriptor in an Immensity of Offered Candidates.”</span> <em>Physical Review Materials</em> 2 (8): 083802. <a href="https://doi.org/10.1103/PhysRevMaterials.2.083802">https://doi.org/10.1103/PhysRevMaterials.2.083802</a>.
</div>
<div id="ref-schweidtmann2019global" class="csl-entry">
Schweidtmann, Artur M., and Alexander Mitsos. 2019. <span>“Deterministic Global Optimization with Artificial Neural Networks Embedded.”</span> <em>Journal of Optimization Theory and Applications</em> 180: 925–48. <a href="https://doi.org/10.1007/s10957-018-1396-0">https://doi.org/10.1007/s10957-018-1396-0</a>.
</div>
<div id="ref-sunshine2025hyperplane" class="csl-entry">
Sunshine, Ethan M., Carolina Colombo Tedesco, Sneha A. Akhade, Matthew J. McNenly, John R. Kitchin, and Carl D. Laird. 2025. <span>“Hyperplane Decision Trees as Piecewise Linear Surrogate Models for Chemical Process Design.”</span> <em>Computers &amp; Chemical Engineering</em> 202: 109204. <a href="https://doi.org/10.1016/j.compchemeng.2025.109204">https://doi.org/10.1016/j.compchemeng.2025.109204</a>.
</div>
<div id="ref-thebelt2021entmoot" class="csl-entry">
Thebelt, Alexander, Jan Kronqvist, Miten Mistry, Robert M. Lee, Nathan Sudermann-Merx, and Ruth Misener. 2021. <span>“<span>ENTMOOT</span>: A Framework for Optimization over Ensemble Tree Models.”</span> <em>Computers &amp; Chemical Engineering</em> 151: 107343. <a href="https://doi.org/10.1016/j.compchemeng.2021.107343">https://doi.org/10.1016/j.compchemeng.2021.107343</a>.
</div>
<div id="ref-tjeng2019evaluating" class="csl-entry">
Tjeng, Vincent, Kai Xiao, and Russ Tedrake. 2019. <span>“Evaluating Robustness of Neural Networks with Mixed Integer Programming.”</span> <em>International Conference on Learning Representations</em>. <a href="https://arxiv.org/abs/1711.07356">https://arxiv.org/abs/1711.07356</a>.
</div>
<div id="ref-vielma2015mixed" class="csl-entry">
Vielma, Juan Pablo. 2015. <span>“Mixed Integer Linear Programming Formulation Techniques.”</span> <em>SIAM Review</em> 57 (1): 3–57. <a href="https://doi.org/10.1137/130915303">https://doi.org/10.1137/130915303</a>.
</div>
</div>


</section>

 ]]></content:encoded></item><item><title>One differentiable model, many tasks</title><link>https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/</link><pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate><category>optimization</category><category>jax</category><category>discopt</category><category>difflow</category><category>digital twins</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/</guid><description>One differentiable model, many tasks</description><content:encoded><![CDATA[ 

Skip to main content




<iframe src="https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/tasks.html" title="One differentiable model, many tasks" style="width:100%; aspect-ratio:1240/920; border:0;" loading="lazy"></iframe>
<p><a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-models/tasks.html">Open the graphic full-size</a>.</p>
<section id="why-differentiable-models" class="level1">
<h1>Why differentiable models?</h1>
<p>If there is one nearly unifying principle in optimization, it is that derivatives play a central role in much of it (I know not all of it; integers are not differentiable, and you often have to estimate derivatives, etc.). Optimization means many things as illustrated above. We can use it to fit parameters, simulate, reconcile data, plan, control, design experiments and more! These all rely on the central model, and different combinations of what know, what you want, and how you get it. What we aim for in our ecosystem shown above is for a single model to be reusable across many tasks.</p>
<p>The graphic above is interactive, and you can see how the task is implemented in many of the tasks through notebook examples. We have not fully integrated everything seamlessly yet, but we are working on it and making good progress. If you find bugs or have suggestions, please let us know by opening an issue on GitHub!</p>
<!--
Draft notes, not rendered.

The idea: the ecosystem gives you a differentiable model f(x; θ). Each task attaches
a different objective and frees a different subset of x, θ and y:

- simulation: x, θ given; solve for y
- parameter estimation: x, y from data; min over θ of a loss
- data reconciliation: noisy y from data; min weighted adjustment s.t. the model
- planning: data plus Δ = ∂y/∂x replaces hand-maintained delta vectors
- control: data plus a goal; re-solve over a horizon
- design of experiments: max information over x
- optimization: min g(x)
- sensitivity and UQ: the "?" in the sketch; ∂y/∂θ and Σ_y ≈ J Σ_θ Jᵀ

One model reused across all of them is what makes it a digital twin.

Candidates for the dashed satellites: state estimation (MHE), model discrimination,
optimization under uncertainty, hybrid models, surrogates, flexibility, fault detection.

Notebooks to write (no example yet):
- discopt: data reconciliation, planning, MPC (receding horizon), state estimation,
  flexibility, fault detection
- difflow: control, design of experiments, state estimation, model discrimination,
  hybrid models, flexibility (docs pages exist for DOE, MHE, flexibility, operability)
-->


</section>

 ]]></content:encoded></item><item><title>The differentiable optimization landscape</title><link>https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0000</pubDate><category>optimization</category><category>jax</category><category>discopt</category><category>difflow</category><category>pounce</category><guid isPermaLink="true">https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/</guid><description>The differentiable optimization landscape</description><content:encoded><![CDATA[ 

Skip to main content




<iframe src="https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/landscape.html" title="The differentiable optimization landscape" style="width:100%; aspect-ratio:1320/930; border:0;" loading="lazy"></iframe>
<p><a href="https://kitchingroup.cheme.cmu.edu/posts/differentiable-optimization-landscape/landscape.html">Open the graphic full-size</a>.</p>
<section id="the-big-pivot-into-optimization" class="level1">
<h1>The big pivot into optimization</h1>
<p>Over the past year I have been making a big pivot in research into optimization. I have always learned by doing, and this post documents some of that journey. This transition has been happening for a while, really starting about a decade ago when I started using machine learning to build surrogate models for DFT. That work led to this <a href="https://kitchingroup.cheme.cmu.edu/papers/kitchin2025/">perspective</a> on differentiable programming in scientific programming. That has led to this year’s work in differentiable optimization. My goal is to build an end-to-end differentiable optimization stack that integrates differentiable programming, differentiable solvers, and differentiable surrogates.</p>
<section id="automatic-differentiation-and-program-based-optimization" class="level2">
<h2 class="anchored" data-anchor-id="automatic-differentiation-and-program-based-optimization">Automatic differentiation and program-based optimization</h2>
<p>Coming from the <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/neural-network-potentials/">machine learned potential</a> field, which uses pytorch and jax with regular Python to solve their optimization problems, that is the approach I started first in <a href="https://github.com/jkitchin/differentiable-flowsheets">difflow</a>. The idea was pretty simple, you write regular Python code, and use jax solvers to solve the problem. I found this appealing because I have decades of programming experience in Python. Of course I was familiar with Pyomo, and although it is written in Python, it is very unPythonic with its operator overloading and heavy object-oriented programming. A year ago I did not fully understand how the solves that Pyomo uses actually have automatic differentiation and I thought jax would enable something they can’t do in cases where you don’t have equations. Agentic coding played a huge role in getting difflow off the ground, I started it shortly after Opus 4.5 was released in Nov 2025 and quickly got a proof of concept working. I learned about sequential modular solves, and that I could reformulate a residual approach that also allows a simultaneous solve. difflow is still being actively developed and fills a part of the landscape that is not easily filled by other tools.</p>
</section>
<section id="interior-point-solvers-and-differentiable-programming" class="level2">
<h2 class="anchored" data-anchor-id="interior-point-solvers-and-differentiable-programming">Interior point solvers and differentiable programming</h2>
<p>In February 2026 I set out a challenge at a GRC conference that as a community we were not thinking big enough about agentic coding. I suggested building a new version of Ipopt in Rust, with a new linear algebra library. I actually thought at the time it would probably not work since I was not familiar with Rust, or interior point methods, and that it would establish the boundaries of what is possible with agentic coding. Many people did not think this would work either, but I tried it anyway, and week after week it kept working better. I did have to abandon the first attempt (ripopt) and start over, but pounce emerged from this as one of the best open source interior point solvers, and built a small community of contributors. pounce is where I learned about the way automatic differentiation works in equation-oriented solvers, and that we can use sensitivity analysis to get differentiable solvers in Python.</p>
</section>
<section id="algebraic-modeling-languages-and-mixed-integer-programming" class="level2">
<h2 class="anchored" data-anchor-id="algebraic-modeling-languages-and-mixed-integer-programming">Algebraic modeling languages and mixed integer programming</h2>
<p>At that same GRC, I build <a href="https://github.com/jkitchin/jaxsr">jaxsr</a> as a symbolic regression modeling tool. This got me interested in mixed integer programming and global optimization, and I started <a href="https://github.com/jkitchin/discopt">discopt</a>. This forced me to get comfortable with the need for an algebraic modeling language and the kinds of analysis that are possible. My original goal in discopt was to also build a full Rust solver for mixed integer programming that leveraged pounce for the NLP parts. I tried that for a while, but currently outsource the mixed integer programming to the open source solver HiGHS. Several people have asked why didn’t I just use Pyomo? The simple answer is I learn by doing, and this is how I did it. I also wanted to try to address some limitations I have heard about Pyomo, primarily performance. For large problems it can take longer to build your model than to solve it. discopt addresses some of those issues, and let me develop part of the ecosystem in the landscape above.</p>
</section>
<section id="differentiable-surrogate-models-and-uncertainty-quantification" class="level2">
<h2 class="anchored" data-anchor-id="differentiable-surrogate-models-and-uncertainty-quantification">Differentiable surrogate models and uncertainty quantification</h2>
<p>In between these efforts we had a need for differentiable surrogate models. I learned in <a href="https://kitchingroup.cheme.cmu.edu/papers/sunshine2025/">this paper</a> that some surrogate models are MIP representable which has desirable properties for optimization. In parallel efforts in molecular simulation we were developing <a href="https://kitchingroup.cheme.cmu.edu/papers/topics/uncertainty-quantification/">uncertainty quantification</a> and this started making its way into our efforts in differentiable surrogates with uncertainty quantification. This means we can build surrogates that have properties we want, e.g.&nbsp;monotonicity, convexity, and uncertainty quantification, MIP representable, etc. and use them in optimization. This is a new paradigm that is not easy with other tools.</p>
</section>
</section>
<section id="why-do-we-need-new-solvers-at-all" class="level1">
<h1>Why do we need new solvers at all?</h1>
<p>There are a plethora of options, very good commercial ones, and open-source ones. I work in higher education, and I think it is important that free, open-source solvers exist for students and researchers. I got into computational catalysis as a student because there was a free code I could use, read, modify and learn from. It is often not possible to do this with closed codes. Even if my codes aren’t as good as other codes, if they open the door for others to learn then a part of the mission is accomplished.</p>
<p>New starts mean new things are possible. pounce is more comprehensive than Ipopt in interior point methods, with integrated sensitivity and other great features that are 1) pip-installable, 2) capable of running in your browser, and with interfaces to use in other interfaces. discopt led to new integrations of ideas, and eventually to the development of jax-go which bridges the gap between program and equation-based optimization. difflow is a new way to think about optimization, and the differentiable surrogates are a new way to think about optimization with uncertainty quantification.</p>
<p>Agentic optimization is a new thing with these solvers. The tight integration of agentic coding with differentiable programming and differentiable solvers is a new paradigm that is more possible with these new solvers because they were designed with this in mind. We have integrated agentic debugging, and <a href="https://youtube.com/live/9JW6elWTYhk">agentic optimization</a>.</p>
</section>
<section id="what-is-next" class="level1">
<h1>What is next?</h1>
<p>I have laid out an ambitious vision for the end-to-end differentiable optimization stack, and I think we are making good progress on it. It is a lot, and we don’t use all of them every day. Some of these tools are stabilizing. Feral is pretty stable now. I probably work on pounce once a week or so. discopt and difflow are still changing fast. It would probably be a lie to say there won’t be new packages coming. I still have many private projects that haven’t seen the light of day yet, but will eventually.</p>
<p>We (the Kitchin group) are beginning to enter a new era of applications. Inspired by the algebraic modeling language we made <a href="https://github.com/jkitchin/discopt-mkm">discopt-mkm</a> for microkinetic modeling, and <a href="https://github.com/jkitchin/discopt-calphad">discopt-calphad</a> for alloy phase behavior. <a href="https://github.com/jkitchin/discopt-doe">discopt-doe</a> is a new direction in design of experiments. We have projects in process modeling, alloy design, rare earth separation, and more.</p>
<p>It is always dangerous to predict the future, but I think 5 years from now we will have a much more integrated landscape of differentiable optimization tools. Right now programmatic automatic differentiation is mostly only available as addons like jax, pytorch in Python, and something like that in Julia. AD is coming to Rust as a native feature in a few years, and predict this will be a game changer for optimization. The pace of development I have demonstrated this year means it will probably only take a year or so to transition from a Python or Julia focused solution space to Rust if we decide to. These changes come with 20/20 hindsight and lessons learned from everything we know now, so they happen very quickly. I think the next 5 years will be very exciting for differentiable optimization.</p>
<p>I still think we are not thinking big enough as a community. I am grateful to a few people who made substantial contributions to these codes: Devin Griffith for major contributions to sensitivity in pounce, Germán Heim for integrating pounce into his modeling language oximo. Milan Rother forked feral into <a href="https://milanrother.com/stack/rslab/">RSLAB</a> and was the inspiration for pounce in the browser. Victor Alves for using pounce before anyone else. David Bernal for major contributions in global optimization in discopt, and ideas linking it to pounce. All this work was done by a very small number of people, and I think we can do better as a community.</p>
<p>It is an open challenge to figure out how to sustain these efforts. Pyomo and IDAES were built over two decades, with generous federal funding. This is a challenge today as that funding becomes more scarce. We need new models to support this kind of work that balances keeping open-access to the code, and generating revenue to support the people who build it.</p>
<p>Along this journey I have come to appreciate that 1) I still have a lot to learn in this space, and 2) there is not one tool to rule in optimization; there are many ways to solve problems, and some ways are better than others for certain problems. That means you have to have a landscape of pathways to choose, and historically I have chosen the roads less traveled. I have learned a lot along the way, and I hope this post helps others to learn from my journey.</p>


</section>

 ]]></content:encoded></item><item>
      <title>Using Custodian to help converge an optimization problem</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2023/09/19/Using-Custodian-to-help-converge-an-optimization-problem</link>
      <pubDate>Tue, 19 Sep 2023 15:34:21 EDT</pubDate>
      <category><![CDATA[optimization]]></category>
      <category><![CDATA[programming]]></category>
      <guid isPermaLink="false">Dx1efJ79cKOKFx-BnlZVFB1wGjA=</guid>
      <description>Using Custodian to help converge an optimization problem</description>
      <content:encoded><![CDATA[


&lt;p&gt;
In high-throughput calculations, some fraction of them usually fail for some reason. Sometimes it is easy to fix these calculations and re-run them successfully, for example, you might just need a different initialization, or to increase memory or the number of allowed steps, etc.  &lt;a href="http://materialsproject.github.io/custodian/"&gt;custodian&lt;/a&gt; is a tool that is designed for this purpose. 
&lt;/p&gt;

&lt;p&gt;
The idea is we make a function to do what we want that has arguments that control that. We need a function that can examine the output of the function and determine if it succeeded, and if it didn't succeed to say what new arguments to try next. Then we run the function in custodian and let it take care of rerunning with new arguments until it either succeeds, or tries too many times.
&lt;/p&gt;

&lt;p&gt;
The goal here is to use &lt;a href="http://materialsproject.github.io/custodian/"&gt;custodian&lt;/a&gt; to fix a problem optimization. The example is a little contrived, we set a number of iterations artificially low so that the minimization fails by reaching the maximum number of iterations. Custodian will catch this, and increase the number of iterations until it succeeds. Here is the objective function:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;objective&lt;/span&gt;(x):
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.exp(x**2) - 10*np.exp(x)

&lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt; = np.linspace(0, 2)
plt.plot(x, objective(x))
plt.xlabel(&lt;span style="color: #008000;"&gt;'x'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'y'&lt;/span&gt;);
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/354616de80c1f529dd249d03f96e5bc023bbd321.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
Clearly there is a minimum near 1.75, but with a bad initial guess, and not enough iterations, an optimizer fails here. We can tell it fails from the message here, and the solution is run it again with more iterations.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; minimize

minimize(objective, 0.0, options={&lt;span style="color: #008000;"&gt;'maxiter'&lt;/span&gt;: 2})
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example" id="org27fcdb3"&gt;
:RESULTS:
  message: Maximum number of iterations has been exceeded.
  success: False
   status: 1
      fun: -36.86289091418059
        x: [ 1.661e+00]
      nit: 2
      jac: [-2.374e-01]
 hess_inv: [[ 6.889e-03]]
     nfev: 20
     njev: 10
:END:
&lt;/pre&gt;

&lt;p&gt;
With Custodian you define a "Job". This is a class with  &lt;code&gt;params&lt;/code&gt; that contain the adjustable arguments in a dictionary, and a &lt;code&gt;run&lt;/code&gt; method that stores the results in the params attribute. This is an important step, because the error handlers only get the params, so you need the results in there to inspect them.
&lt;/p&gt;

&lt;p&gt;
The error handlers are another class with a &lt;code&gt;check&lt;/code&gt; method that returns True if you should rerun, and a &lt;code&gt;correct&lt;/code&gt; method that sets the params to new values to try next. It seems to return some information about what happened. In the &lt;code&gt;correct&lt;/code&gt; method, we double the maximum number of iterations allowed, and use the last solution point that failed as the initial guess for the next run.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; custodian.custodian &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; Custodian, Job, ErrorHandler

&lt;span style="color: #0000FF;"&gt;class&lt;/span&gt; &lt;span style="color: #6434A3;"&gt;Minimizer&lt;/span&gt;(Job):
    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;__init__&lt;/span&gt;(&lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;, params=&lt;span style="color: #D0372D;"&gt;None&lt;/span&gt;):
        &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params = params &lt;span style="color: #0000FF;"&gt;if&lt;/span&gt; params &lt;span style="color: #0000FF;"&gt;else&lt;/span&gt; {&lt;span style="color: #008000;"&gt;'maxiter'&lt;/span&gt;: 2, &lt;span style="color: #008000;"&gt;'x0'&lt;/span&gt;: 0}
        
    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;run&lt;/span&gt;(&lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;):
        sol = minimize(objective,
                       &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'x0'&lt;/span&gt;],
                       options={&lt;span style="color: #008000;"&gt;'maxiter'&lt;/span&gt;: &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'maxiter'&lt;/span&gt;]})
        &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'sol'&lt;/span&gt;] = sol

&lt;span style="color: #0000FF;"&gt;class&lt;/span&gt; &lt;span style="color: #6434A3;"&gt;MaximumIterationsExceeded&lt;/span&gt;(ErrorHandler):
    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;__init__&lt;/span&gt;(&lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;, params):
        &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params = params

    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;check&lt;/span&gt;(&lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;):
        &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'sol'&lt;/span&gt;].message == &lt;span style="color: #008000;"&gt;'Maximum number of iterations has been exceeded.'&lt;/span&gt;

    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;correct&lt;/span&gt;(&lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;):
        &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'maxiter'&lt;/span&gt;] *= 2
        &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'x0'&lt;/span&gt;] = &lt;span style="color: #0000FF;"&gt;self&lt;/span&gt;.params[&lt;span style="color: #008000;"&gt;'sol'&lt;/span&gt;].x        
        &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; {&lt;span style="color: #008000;"&gt;'errors'&lt;/span&gt;: &lt;span style="color: #008000;"&gt;'MaximumIterations Exceeded'&lt;/span&gt;,
                &lt;span style="color: #008000;"&gt;'actions'&lt;/span&gt;: &lt;span style="color: #008000;"&gt;'maxiter = {self.params["maxiter"]}, x0 = {self.params["x0"]}'&lt;/span&gt;}
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Now we setup the initial params to try, create a Custodian object with the handler and job, and then run it. The results and final params are stored in the params object.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #BA36A5;"&gt;params&lt;/span&gt; = {&lt;span style="color: #008000;"&gt;'maxiter'&lt;/span&gt;: 1, &lt;span style="color: #008000;"&gt;'x0'&lt;/span&gt;: 0}

&lt;span style="color: #BA36A5;"&gt;c&lt;/span&gt; = Custodian([MaximumIterationsExceeded(params)],
              [Minimizer(params)],
               max_errors=5)

c.run()
&lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; key &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; params:
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(key, params[key])
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example" id="orgca61279"&gt;
MaximumIterationsExceeded
MaximumIterationsExceeded
maxiter 4
x0 [1.66250127]
sol   message: Optimization terminated successfully.
  success: True
   status: 0
      fun: -36.86307468296398
        x: [ 1.662e+00]
      nit: 1
      jac: [-9.060e-06]
 hess_inv: [[1]]
     nfev: 6
     njev: 3
&lt;/pre&gt;



&lt;p&gt;
Note that params is modified, and finally has the maxiter value that worked, and the solution in it. You can see we had to rerun this problem  twice before it succeeded, but this happened automatically after the setup. This example is easy because we can simply increase the maxiter value, and no serious logic is needed. Other use cases might include try it again with another solver, try again with a different initial guess, etc. 
&lt;/p&gt;

&lt;p&gt;
It feels a little heavyweight to define the classes, and to store the results in params here, but this was overall under an hour of work to put it all together, starting from scratch with the &lt;a href="http://materialsproject.github.io/custodian/"&gt;Custodian documentation&lt;/a&gt; from the example on the front page. You can do more sophisticated things, including having multiple error handlers. Overall, for a package designed for molecular simulations, this worked well for a different kind of problem.
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2023 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2023/09/19/Using-Custodian-to-help-converge-an-optimization-problem.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.7-pre&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Update on finding the minimum distance from a point to a curve</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2023/02/17/Update-on-finding-the-minimum-distance-from-a-point-to-a-curve</link>
      <pubDate>Fri, 17 Feb 2023 18:41:43 EST</pubDate>
      <category><![CDATA[optimization]]></category>
      <guid isPermaLink="false">rnWiAoFmft_vKbcPjh7HXe08a8c=</guid>
      <description>Update on finding the minimum distance from a point to a curve</description>
      <content:encoded><![CDATA[


&lt;p&gt;
Almost 10 years ago I &lt;a href="https://kitchingroup.cheme.cmu.edu/blog/2013/02/14/Find-the-minimum-distance-from-a-point-to-a-curve/"&gt;wrote&lt;/a&gt; about finding the minimum distance from a point to a curve using a constrained optimization. At that time, the way to do this used &lt;code&gt;scipy.optimize.fmin_coblya&lt;/code&gt;. I learned today from a student, that sometimes this method fails! I reproduce the code here, updated for Python 3, some style updates, and to show it does indeed fail sometimes, notably when the point is "outside" the parabola.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; fmin_cobyla

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f&lt;/span&gt;(x):
    &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x**2

&lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; P &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; np.array([[0.5, 2],
                   [2, 2],
                   [-1, 2],
                   [-2, 2],
                   [0, 0.5],
                   [0, -0.5]]):
    
    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;objective&lt;/span&gt;(X):
        &lt;span style="color: #BA36A5;"&gt;X&lt;/span&gt; = np.array(X)
        &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.linalg.norm(X - P)

    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;c1&lt;/span&gt;(X):
        &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;,&lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt; = X
        &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; f(x) - y

    &lt;span style="color: #BA36A5;"&gt;X&lt;/span&gt; = fmin_cobyla(objective, x0=[P[0], f(P[0])], cons=[c1])

    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'The minimum distance is &lt;/span&gt;{objective(X):1.2f}&lt;span style="color: #008000;"&gt;. Constraint satisfied = &lt;/span&gt;{c1(X) &amp;lt; 1e-6}&lt;span style="color: #008000;"&gt;'&lt;/span&gt;)

    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Verify the vector to this point is normal to the tangent of the curve&lt;/span&gt;
    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;position vector from curve to point&lt;/span&gt;
    v1 = np.array(P) - np.array(X)
    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;position vector&lt;/span&gt;
    v2 = np.array([1, 2.0 * X[0]])
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'dot(v1, v2) = '&lt;/span&gt;, np.dot(v1, v2))

    x = np.linspace(-2, 2, 100)

    plt.plot(x, f(x), &lt;span style="color: #008000;"&gt;'r-'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'f(x)'&lt;/span&gt;)
    plt.plot(P[0], P[1], &lt;span style="color: #008000;"&gt;'bo'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'point'&lt;/span&gt;)
    plt.plot([P[0], X[0]], [P[1], X[1]], &lt;span style="color: #008000;"&gt;'b-'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'shortest distance'&lt;/span&gt;)
    plt.plot([X[0], X[0] + 1], [X[1], X[1] + 2.0 * X[0]], &lt;span style="color: #008000;"&gt;'g-'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'tangent'&lt;/span&gt;)
    plt.axis(&lt;span style="color: #008000;"&gt;'equal'&lt;/span&gt;)
    plt.xlabel(&lt;span style="color: #008000;"&gt;'x'&lt;/span&gt;)
    plt.ylabel(&lt;span style="color: #008000;"&gt;'y'&lt;/span&gt;)    
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The minimum distance is 0.86. Constraint satisfied = True
dot(v1, v2) =  0.0002913487659186309
The minimum distance is 0.00. Constraint satisfied = False
dot(v1, v2) =  0.00021460906432962284
The minimum distance is 0.39. Constraint satisfied = True
dot(v1, v2) =  0.00014271520451364372
The minimum distance is 0.00. Constraint satisfied = False
dot(v1, v2) =  -0.0004089466778209598
The minimum distance is 0.50. Constraint satisfied = True
dot(v1, v2) =  1.9999998429305957e-12
The minimum distance is 0.00. Constraint satisfied = False
dot(v1, v2) =  8.588744170160093e-06
&lt;figure&gt;&lt;img src="/media/f66cff16ba65526d5877bd894142fa021c51f434.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
So, sure enough, the optimizer is failing to find a solution that meets the constraint. It is strange it does not work on the outside. That is almost certainly an algorithm problem. Here we solve it nearly identically with the more modern &lt;code&gt;scipy.optimize.minimize&lt;/code&gt; function, and it converges every time. 
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-jupyter-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; minimize

&lt;span style="color: #0000FF;"&gt;for&lt;/span&gt; P &lt;span style="color: #0000FF;"&gt;in&lt;/span&gt; np.array([[0.5, 2],
                   [2, 2],
                   [-1, 2],
                   [-2, 2],
                   [0, 0.5],
                   [0, -0.5]]):
    
    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;objective&lt;/span&gt;(X):
        &lt;span style="color: #BA36A5;"&gt;X&lt;/span&gt; = np.array(X)
        &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; np.linalg.norm(X - P)

    &lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;c1&lt;/span&gt;(X):
        &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;,&lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt; = X
        &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; f(x) - y

    &lt;span style="color: #BA36A5;"&gt;sol&lt;/span&gt; = minimize(objective, x0=[P[0], f(P[0])], constraints={&lt;span style="color: #008000;"&gt;'type'&lt;/span&gt;: &lt;span style="color: #008000;"&gt;'eq'&lt;/span&gt;, &lt;span style="color: #008000;"&gt;'fun'&lt;/span&gt;: c1})
    X = sol.x

    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'The minimum distance is &lt;/span&gt;{objective(X):1.2f}&lt;span style="color: #008000;"&gt;. Constraint satisfied = &lt;/span&gt;{sol.status &amp;lt; 1e-6}&lt;span style="color: #008000;"&gt;'&lt;/span&gt;)

    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Verify the vector to this point is normal to the tangent of the curve&lt;/span&gt;
    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;position vector from curve to point&lt;/span&gt;
    v1 = np.array(P) - np.array(X)
    &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;position vector&lt;/span&gt;
    v2 = np.array([1, 2.0 * X[0]])
    &lt;span style="color: #006FE0;"&gt;print&lt;/span&gt;(&lt;span style="color: #008000;"&gt;'dot(v1, v2) = '&lt;/span&gt;, np.dot(v1, v2))

    x = np.linspace(-2, 2, 100)

    plt.plot(x, f(x), &lt;span style="color: #008000;"&gt;'r-'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'f(x)'&lt;/span&gt;)
    plt.plot(P[0], P[1], &lt;span style="color: #008000;"&gt;'bo'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'point'&lt;/span&gt;)
    plt.plot([P[0], X[0]], [P[1], X[1]], &lt;span style="color: #008000;"&gt;'b-'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'shortest distance'&lt;/span&gt;)
    plt.plot([X[0], X[0] + 1], [X[1], X[1] + 2.0 * X[0]], &lt;span style="color: #008000;"&gt;'g-'&lt;/span&gt;, label=&lt;span style="color: #008000;"&gt;'tangent'&lt;/span&gt;)
    plt.axis(&lt;span style="color: #008000;"&gt;'equal'&lt;/span&gt;)
    plt.xlabel(&lt;span style="color: #008000;"&gt;'x'&lt;/span&gt;)
    plt.ylabel(&lt;span style="color: #008000;"&gt;'y'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The minimum distance is 0.86. Constraint satisfied = True
dot(v1, v2) =  1.0701251773603815e-08
The minimum distance is 0.55. Constraint satisfied = True
dot(v1, v2) =  -0.0005793028003104883
The minimum distance is 0.39. Constraint satisfied = True
dot(v1, v2) =  -1.869272921939391e-05
The minimum distance is 0.55. Constraint satisfied = True
dot(v1, v2) =  0.0005792953298950909
The minimum distance is 0.50. Constraint satisfied = True
dot(v1, v2) =  0.0
The minimum distance is 0.50. Constraint satisfied = True
dot(v1, v2) =  0.0
&lt;figure&gt;&lt;img src="/media/4776aa1f11411aa8cf0c3ea47f96e2a8973e314e.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
There is no wisdom in fixing the first problem, here I just tried a newer optimization method. Out of the box with default settings it just worked. I did learn the answer is sensitive to the initial guess, so it could make sense to sample the function and find the point that is closest as the initial guess, but here the simple heuristic guess I used worked fine.
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2023 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2023/02/17/Update-on-finding-the-minimum-distance-from-a-point-to-a-curve.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.5.5&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Constrained optimization with Lagrange multipliers and autograd</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2018/11/03/Constrained-optimization-with-Lagrange-multipliers-and-autograd</link>
      <pubDate>Sat, 03 Nov 2018 09:39:20 EDT</pubDate>
      <category><![CDATA[autograd]]></category>
      <category><![CDATA[optimization]]></category>
      <guid isPermaLink="false">Iy0PvHLUa3-zADMW_uQnOCfZ6Z4=</guid>
      <description>Constrained optimization with Lagrange multipliers and autograd</description>
      <content:encoded><![CDATA[


&lt;p&gt;
Constrained optimization is common in engineering problems solving. A prototypical example (from Greenberg, Advanced Engineering Mathematics, Ch 13.7) is to find the point on a plane that is closest to the origin. The plane is defined by the equation \(2x - y + z = 3\), and we seek to minimize \(x^2 + y^2 + z^2\) subject to the equality constraint defined by the plane. &lt;code&gt;scipy.optimize.minimize&lt;/code&gt; provides a pretty convenient interface to solve a problem like this, ans shown here.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; minimize

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;objective&lt;/span&gt;(X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; x**2 + y**2 + z**2

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;eq&lt;/span&gt;(X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 2 * x - y + z - 3

&lt;span style="color: #BA36A5;"&gt;sol&lt;/span&gt; = minimize(objective, [1, -0.5, 0.5], constraints={&lt;span style="color: #008000;"&gt;'type'&lt;/span&gt;: &lt;span style="color: #008000;"&gt;'eq'&lt;/span&gt;, &lt;span style="color: #008000;"&gt;'fun'&lt;/span&gt;: eq})
sol
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
    fun: 1.5
    jac: array([ 2.00000001, -0.99999999,  1.00000001])
message: 'Optimization terminated successfully.'
   nfev: 5
    nit: 1
   njev: 1
 status: 0
success: True
      x: array([ 1. , -0.5,  0.5])

&lt;/pre&gt;

&lt;p&gt;
I like the minimize function a lot, although I am not crazy for how the constraints are provided. The alternative used to be that there was an argument for equality constraints and another for inequality constraints. Analogous to &lt;code&gt;scipy.integrate.solve_ivp&lt;/code&gt; event functions, they could have also used function attributes.
&lt;/p&gt;

&lt;p&gt;
Sometimes, it might be desirable to go back to basics though, especially if you are unaware of the &lt;code&gt;minimize&lt;/code&gt; function or perhaps suspect it is not working right and want an independent answer. Next we look at how to construct this constrained optimization problem using Lagrange multipliers. This converts the problem into an augmented unconstrained optimization problem we can use &lt;code&gt;fsolve&lt;/code&gt; on. The gist of this method is we formulate a new problem:
&lt;/p&gt;

&lt;p&gt;
\(F(X) = f(X) - \lambda g(X)\)
&lt;/p&gt;

&lt;p&gt;
and then solve the simultaneous resulting equations:
&lt;/p&gt;

&lt;p&gt;
\(F_x(X) = F_y(X) = F_z(X) = g(X) = 0\) where \(F_x\) is the derivative of \(f*\) with respect to \(x\), and \(g(X)\) is the equality constraint written so it is equal to zero. Since we end up with four equations that equal zero, we can simply use fsolve to get the solution. Many &lt;a href="http://kitchingroup.cheme.cmu.edu/blog/2013/02/03/Using-Lagrange-multipliers-in-optimization/"&gt;years ago&lt;/a&gt; I used a finite difference approximation to the derivatives. Today we use autograd to get the desired derivatives. Here it is.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; autograd.numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; grad

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;F&lt;/span&gt;(L):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #036A07;"&gt;'Augmented Lagrange function'&lt;/span&gt;
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;_lambda&lt;/span&gt; = L
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; objective([x, y, z]) - _lambda * eq([x, y, z])

&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Gradients of the Lagrange function&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;dfdL&lt;/span&gt; = grad(F, 0)

&lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;Find L that returns all zeros in this function.&lt;/span&gt;
&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;obj&lt;/span&gt;(L):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;_lambda&lt;/span&gt; = L
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;dFdx&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;dFdy&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;dFdz&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;dFdlam&lt;/span&gt; = dfdL(L)
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; [dFdx, dFdy, dFdz, eq([x, y, z])]

&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; fsolve
&lt;span style="color: #BA36A5;"&gt;x&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;y&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;z&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;_lam&lt;/span&gt; = fsolve(obj, [0.0, 0.0, 0.0, 1.0])
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'The answer is at {x, y, z}'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
The answer is at (1.0, -0.5, 0.5)


&lt;/pre&gt;

&lt;p&gt;
That is the same answer as before. Note we have still relied on some black box solver inside of fsolve (instead of inside minimize), but it might be more clear what problem we are solving (e.g. finding zeros). It takes a bit more work to set this up, since we have to construct the augmented function, but autograd makes it pretty convenient to set up the final objective function we want to solve.
&lt;/p&gt;

&lt;p&gt;
How do we know we are at a minimum? We can check that the Hessian is positive definite in the original function we wanted to minimize. You can see here the array is positive definite, e.g. all the eigenvalues are positive. autograd makes this easy too.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; hessian
&lt;span style="color: #BA36A5;"&gt;h&lt;/span&gt; = hessian(objective, 0)
h(np.array([x, y, z]))
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
array([[ 2.,  0.,  0.],
       [ 0.,  2.,  0.],
       [ 0.,  0.,  2.]])

&lt;/pre&gt;

&lt;p&gt;
In case it isn't evident from that structure that the eigenvalues are all positive, here we compute them:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;np.linalg.eig(h(np.array([x, y, z])))[0]
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
array([ 2.,  2.,  2.])

&lt;/pre&gt;

&lt;p&gt;
In summary, autograd continues to enable advanced engineering problems to be solved.
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2018 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2018/11/03/Constrained-optimization-with-Lagrange-multipliers-and-autograd.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.1.14&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Finding the maximum power of a photovoltaic device.</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2014/04/15/Finding-the-maximum-power-of-a-photovoltaic-device</link>
      <pubDate>Tue, 15 Apr 2014 20:38:10 EDT</pubDate>
      <category><![CDATA[optimization]]></category>
      <category><![CDATA[python]]></category>
      <guid isPermaLink="false">N6khYFE0CgFhCKOJcujrM9MZ7WA=</guid>
      <description>Finding the maximum power of a photovoltaic device.</description>
      <content:encoded><![CDATA[



&lt;p&gt;
A photovoltaic device is characterized by a current-voltage relationship. Let us say, for argument's sake, that the relationship is known and defined by
&lt;/p&gt;

&lt;p&gt;
\(i = 0.5 - 0.5 * V^2\)
&lt;/p&gt;

&lt;p&gt;
The voltage is highest when the current is equal to zero, but of course then you get no power. The current is highest when the voltage is zero, i.e. short-circuited, but there is again no power. We seek the highest power condition, which is to find the maximum of \(i V\). This is a constrained optimization. We solve it by creating an objective function that returns the negative of (\i V\), and then find the minimum.
&lt;/p&gt;

&lt;p&gt;
First, let us examine the i-V relationship.
&lt;/p&gt;
&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np

&lt;span style="color: #BA36A5;"&gt;V&lt;/span&gt; = np.linspace(0, 1)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;i&lt;/span&gt;(V):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 0.5 - 0.5 * V**2

plt.figure()
plt.plot(V, i(V))
plt.savefig(&lt;span style="color: #008000;"&gt;'images/iV.png'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
&amp;lt;matplotlib.figure.Figure object at 0x11193ec18&amp;gt;
[&amp;lt;matplotlib.lines.Line2D object at 0x111d43668&amp;gt;]
&lt;/pre&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="/media/2014-04-15-Finding-the-maximum-power-of-a-photovoltaic-device./iV.png"&gt; 
&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;
Now, let us be sure there is a maximum in power.
&lt;/p&gt;
&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np

&lt;span style="color: #BA36A5;"&gt;V&lt;/span&gt; = np.linspace(0, 1)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;i&lt;/span&gt;(V):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 0.5 - 0.5 * V**2

plt.plot(V, i(V) * V)
plt.savefig(&lt;span style="color: #008000;"&gt;'images/P1.png'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
[&amp;lt;matplotlib.lines.Line2D object at 0x111d437f0&amp;gt;]
&lt;/pre&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="/media/2014-04-15-Finding-the-maximum-power-of-a-photovoltaic-device./P1.png"&gt; 
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
You can see in fact there is a maximum, near V=0.6. We could solve this problem analytically by taking the appropriate derivative and solving it for zero. That still might require solving a nonlinear problem though. We will directly setup and solve the constrained optimization.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; fmin_slsqp
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;objective&lt;/span&gt;(X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;i&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;V&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; - i * V

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;eqc&lt;/span&gt;(X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #036A07;"&gt;'equality constraint'&lt;/span&gt;
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;i&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;V&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; (0.5 - 0.5 * V**2) - i

&lt;span style="color: #BA36A5;"&gt;X0&lt;/span&gt; = [0.2, 0.6]
&lt;span style="color: #BA36A5;"&gt;X&lt;/span&gt; = fmin_slsqp(objective, X0, eqcons=[eqc])

&lt;span style="color: #BA36A5;"&gt;imax&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;Vmax&lt;/span&gt; = X


&lt;span style="color: #BA36A5;"&gt;V&lt;/span&gt; = np.linspace(0, 1)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;i&lt;/span&gt;(V):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 0.5 - 0.5 * V**2

plt.plot(V, i(V), Vmax, imax, &lt;span style="color: #008000;"&gt;'ro'&lt;/span&gt;)
plt.savefig(&lt;span style="color: #008000;"&gt;'images/P2.png'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
Optimization terminated successfully.    (Exit mode 0)
            Current function value: -0.192450127337
            Iterations: 5
            Function evaluations: 20
            Gradient evaluations: 5
[&amp;lt;matplotlib.lines.Line2D object at 0x111946470&amp;gt;, &amp;lt;matplotlib.lines.Line2D object at 0x11192c518&amp;gt;]
&lt;/pre&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="/media/2014-04-15-Finding-the-maximum-power-of-a-photovoltaic-device./P2.png"&gt; 
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
You can see the maximum power is approximately 0.2 (unspecified units), at the conditions indicated by the red dot in the figure above.
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2016 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;
&lt;p&gt;&lt;a href="/org/2014/04/15/Finding-the-maximum-power-of-a-photovoltaic-device..org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 8.2.10&lt;/p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Constrained fits to data</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2013/06/11/Constrained-fits-to-data</link>
      <pubDate>Tue, 11 Jun 2013 19:39:59 EDT</pubDate>
      <category><![CDATA[optimization]]></category>
      <category><![CDATA[data analysis]]></category>
      <guid isPermaLink="false">tkPJI7DfYBi--ElDtj1PvwvMk08=</guid>
      <description>Constrained fits to data</description>
      <content:encoded><![CDATA[


&lt;p&gt;
Our objective here is to fit a quadratic function in the least squares sense to some data, but we want to constrain the fit so that the function has specific values at the end-points. The application is to fit a function to the lattice constant of an alloy at different compositions. We constrain the fit because we know the lattice constant of the pure metals, which are at the end-points of the fit and we want these to be correct. 
&lt;/p&gt;

&lt;p&gt;
We define the alloy composition in terms of the mole fraction of one species, e.g. \(A_xB_{1-x}\). For \(x=0\), the alloy is pure B, whereas for \(x=1\) the alloy is pure A. According to Vegard's law the lattice constant is a linear composition weighted average of the pure component lattice constants, but sometimes small deviations are observed. Here we will fit a quadratic function that is constrained to give the pure metal component lattice constants at the end points. 
&lt;/p&gt;

&lt;p&gt;
The quadratic function is \(y = a x^2 + b x + c\). One constraint is at \(x=0\) where \(y = c\), or \(c\) is the lattice constant of pure B. The second constraint is at \(x=1\), where \(a + b + c\) is equal to the lattice constant of pure A. Thus, there is only one degree of freedom. \(c = LC_B\), and \(b = LC_A - c - a\), so \(a\) is our only variable.
&lt;/p&gt;

&lt;p&gt;
We will solve this problem by minimizing the summed squared error between the fit and the data. We use the &lt;code&gt;fmin&lt;/code&gt; function in &lt;code&gt;scipy.optimize&lt;/code&gt;. First we create a fit function that encodes the constraints. Then we create an objective function that will be minimized. We have to make a guess about the value of \(a\) that minimizes the summed squared error. A line fits the data moderately well, so we guess a small value, i.e. near zero, for \(a\). Here is the solution.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #8b0000;"&gt;as&lt;/span&gt; np
&lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #8b0000;"&gt;as&lt;/span&gt; plt

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;Data to fit to&lt;/span&gt;
&lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;x=0 is pure B&lt;/span&gt;
&lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;x=1 is pure A&lt;/span&gt;
X = np.array([0.0, 0.1,  0.25, 0.5,  0.6,  0.8,  1.0])
Y = np.array([3.9, 3.89, 3.87, 3.78, 3.75, 3.69, 3.6])

&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;func&lt;/span&gt;(a, XX):
    LC_A = 3.6
    LC_B = 3.9

    c = LC_B
    b = LC_A - c - a

    yfit = a * XX**2 + b * XX + c
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; yfit

&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;objective&lt;/span&gt;(a):
    &lt;span style="color: #228b22;"&gt;'function to minimize'&lt;/span&gt;
    SSE = np.sum((Y - func(a, X))**2)
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; SSE


&lt;span style="color: #8b0000;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; fmin

a_fit = fmin(objective, 0)
plt.plot(X, Y, &lt;span style="color: #228b22;"&gt;'bo '&lt;/span&gt;)

x = np.linspace(0, 1)
plt.plot(x, func(a_fit, x))
plt.savefig(&lt;span style="color: #228b22;"&gt;'images/constrained-quadratic-fit.png'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
Optimization terminated successfully.
         Current function value: 0.000445
         Iterations: 19
         Function evaluations: 38
&lt;/pre&gt;

&lt;p&gt;
Here is the result:
&lt;p&gt;&lt;img src="/img/./images/constrained-quadratic-fit.png"&gt;&lt;p&gt;
&lt;/p&gt;

&lt;p&gt;
You can see that the end points go through the end-points as prescribed. 
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2013 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;&lt;p&gt;&lt;a href="/org/2013/06/11/Constrained-fits-to-data.org"&gt;org-mode source&lt;/a&gt;&lt;p&gt;]]></content:encoded>
    </item>
    <item>
      <title>Gibbs energy minimization and the NIST webbook</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2013/03/01/Gibbs-energy-minimization-and-the-NIST-webbook</link>
      <pubDate>Fri, 01 Mar 2013 13:11:58 EST</pubDate>
      <category><![CDATA[optimization]]></category>
      <guid isPermaLink="false">2cPyJ1dhosp_-EMfiMiuxmC2KMM=</guid>
      <description>Gibbs energy minimization and the NIST webbook</description>
      <content:encoded><![CDATA[


&lt;p&gt;
&lt;a href="http://matlab.cheme.cmu.edu/2011/12/25/gibbs-energy-minimization-and-the-nist-webbook/" &gt;Matlab post&lt;/a&gt;
In Post 1536 we used the NIST webbook to compute a temperature dependent Gibbs energy of reaction, and then used a reaction extent variable to compute the equilibrium concentrations of each species for the water gas shift reaction.
&lt;/p&gt;

&lt;p&gt;
Today, we look at the direct minimization of the Gibbs free energy of the species, with no assumptions about stoichiometry of reactions. We only apply the constraint of conservation of atoms. We use the NIST Webbook to provide the data for the Gibbs energy of each species.
&lt;/p&gt;

&lt;p&gt;
As a reminder we consider equilibrium between the species \(CO\), \(H_2O\), \(CO_2\) and \(H_2\), at 1000K, and 10 atm total pressure with an initial equimolar molar flow rate of \(CO\) and \(H_2O\).
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #8b0000;"&gt;as&lt;/span&gt; np

T = 1000  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# K&lt;/span&gt;
R = 8.314e-3 &lt;span style="color: #ff0000; font-weight: bold;"&gt;# kJ/mol/K&lt;/span&gt;

P = 10.0 &lt;span style="color: #ff0000; font-weight: bold;"&gt;# atm, this is the total pressure in the reactor&lt;/span&gt;
Po = 1.0 &lt;span style="color: #ff0000; font-weight: bold;"&gt;# atm, this is the standard state pressure&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
We are going to store all the data and calculations in vectors, so we need to assign each position in the vector to a species. Here are the definitions we use in this work.
&lt;/p&gt;

&lt;pre class="example"&gt;
1  CO
2  H2O
3  CO2
4  H2
&lt;/pre&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;species = [&lt;span style="color: #228b22;"&gt;'CO'&lt;/span&gt;, &lt;span style="color: #228b22;"&gt;'H2O'&lt;/span&gt;, &lt;span style="color: #228b22;"&gt;'CO2'&lt;/span&gt;, &lt;span style="color: #228b22;"&gt;'H2'&lt;/span&gt;]

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# Heats of formation at 298.15 K&lt;/span&gt;

Hf298 = [
    -110.53,  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# CO&lt;/span&gt;
    -241.826, &lt;span style="color: #ff0000; font-weight: bold;"&gt;# H2O&lt;/span&gt;
    -393.51,  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# CO2&lt;/span&gt;
       0.0]   &lt;span style="color: #ff0000; font-weight: bold;"&gt;# H2&lt;/span&gt;

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# Shomate parameters for each species&lt;/span&gt;
&lt;span style="color: #ff0000; font-weight: bold;"&gt;#           A          B           C          D          E            F          G       H&lt;/span&gt;
WB = [[25.56759,  6.096130,     4.054656,  -2.671301,  0.131021, -118.0089, 227.3665,   -110.5271],  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# CO&lt;/span&gt;
      [30.09200,  6.832514,     6.793435,  -2.534480,  0.082139, -250.8810, 223.3967,   -241.8264],  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# H2O&lt;/span&gt;
      [24.99735,  55.18696,   -33.69137,    7.948387, -0.136638, -403.6075, 228.2431,   -393.5224],  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# CO2&lt;/span&gt;
      [33.066178, -11.363417,  11.432816,  -2.772874, -0.158558, -9.980797, 172.707974,    0.0]]     &lt;span style="color: #ff0000; font-weight: bold;"&gt;# H2&lt;/span&gt;

WB = np.array(WB)

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# Shomate equations&lt;/span&gt;
t = T/1000
T_H = np.array([t,  t**2 / 2.0, t**3 / 3.0, t**4 / 4.0, -1.0 / t, 1.0, 0.0, -1.0])
T_S = np.array([np.log(t), t,  t**2 / 2.0,  t**3 / 3.0, -1.0 / (2.0 * t**2), 0.0, 1.0, 0.0])

H = np.dot(WB, T_H)        &lt;span style="color: #ff0000; font-weight: bold;"&gt;# (H - H_298.15) kJ/mol&lt;/span&gt;
S = np.dot(WB, T_S/1000.0) &lt;span style="color: #ff0000; font-weight: bold;"&gt;# absolute entropy kJ/mol/K&lt;/span&gt;

Gjo = Hf298 + H - T*S      &lt;span style="color: #ff0000; font-weight: bold;"&gt;# Gibbs energy of each component at 1000 K&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Now, construct the Gibbs free energy function, accounting for the change in activity due to concentration changes (ideal mixing).
&lt;/p&gt;
&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;func&lt;/span&gt;(nj):
    nj = np.array(nj)
    Enj = np.sum(nj);
    Gj =  Gjo / (R * T) + np.log(nj / Enj * P / Po)
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; np.dot(nj, Gj)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
We impose the constraint that all atoms are conserved from the initial conditions to the equilibrium distribution of species. These constraints are in the form of \(A_{eq} n = b_{eq}\), where \(n\) is the vector of mole numbers for each species.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;Aeq = np.array([[ 1,    0,    1,    0],  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# C balance&lt;/span&gt;
                [ 1,    1,    2,    0],  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# O balance&lt;/span&gt;
                [ 0,    2,    0,    2]]) &lt;span style="color: #ff0000; font-weight: bold;"&gt;# H balance&lt;/span&gt;

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# equimolar feed of 1 mol H2O and 1 mol CO&lt;/span&gt;
beq = np.array([1,  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# mol C fed&lt;/span&gt;
                2,  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# mol O fed&lt;/span&gt;
                2]) &lt;span style="color: #ff0000; font-weight: bold;"&gt;# mol H fed&lt;/span&gt;

&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;ec1&lt;/span&gt;(nj):
    &lt;span style="color: #228b22;"&gt;'conservation of atoms constraint'&lt;/span&gt;
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; np.dot(Aeq, nj) - beq
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Now we are ready to solve the problem. 
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; fmin_slsqp

n0 = [0.5, 0.5, 0.5, 0.5]  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# initial guesses&lt;/span&gt;
N = fmin_slsqp(func, n0, f_eqcons=ec1)
&lt;span style="color: #8b0000;"&gt;print&lt;/span&gt; N
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
&amp;gt;&amp;gt;&amp;gt; &amp;gt;&amp;gt;&amp;gt; Optimization terminated successfully.    (Exit mode 0)
            Current function value: -91.204832308
            Iterations: 2
            Function evaluations: 13
            Gradient evaluations: 2
[ 0.45502309  0.45502309  0.54497691  0.54497691]
&lt;/pre&gt;

&lt;div id="outline-container-1" class="outline-2"&gt;
&lt;h2 id="sec-1"&gt;&lt;span class="section-number-2"&gt;1&lt;/span&gt; Compute mole fractions and partial pressures&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
The pressures here are in good agreement with the pressures found by other methods. The minor disagreement (in the third or fourth decimal place) is likely due to convergence tolerances in the different algorithms used.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;yj = N / np.sum(N)
Pj = yj * P

&lt;span style="color: #8b0000;"&gt;for&lt;/span&gt; s, y, p &lt;span style="color: #8b0000;"&gt;in&lt;/span&gt; &lt;span style="color: #8b0000;"&gt;zip&lt;/span&gt;(species, yj, Pj):
    &lt;span style="color: #8b0000;"&gt;print&lt;/span&gt; &lt;span style="color: #228b22;"&gt;'{0:10s}: {1:1.2f} {2:1.2f}'&lt;/span&gt;.format(s, y, p)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
&amp;gt;&amp;gt;&amp;gt; &amp;gt;&amp;gt;&amp;gt; ... ... CO        : 0.23 2.28
H2O       : 0.23 2.28
CO2       : 0.27 2.72
H2        : 0.27 2.72
&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="outline-container-2" class="outline-2"&gt;
&lt;h2 id="sec-2"&gt;&lt;span class="section-number-2"&gt;2&lt;/span&gt; Computing equilibrium constants&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
We can compute the equilibrium constant for the reaction \(CO + H_2O \rightleftharpoons CO_2 + H_2\). Compared to the value of K = 1.44 we found at the end of Post 1536 , the agreement is excellent. Note, that to define an equilibrium constant it is necessary to specify a reaction, even though it is not necessary to even consider a reaction to obtain the equilibrium distribution of species!
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;nuj = np.array([-1, -1, 1, 1])  &lt;span style="color: #ff0000; font-weight: bold;"&gt;# stoichiometric coefficients of the reaction&lt;/span&gt;
K = np.prod(yj**nuj)
&lt;span style="color: #8b0000;"&gt;print&lt;/span&gt; K
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
&amp;gt;&amp;gt;&amp;gt; 1.43446295961
&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Copyright (C) 2013 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;&lt;p&gt;&lt;a href="/org/2013/03/01/Gibbs-energy-minimization-and-the-NIST-webbook.org"&gt;org-mode source&lt;/a&gt;&lt;p&gt;]]></content:encoded>
    </item>
    </channel>
</rss>