From a433c5c41dabeda998924fdde5b72d324f7a179c Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Thu, 26 Mar 2026 16:17:01 +0000
Subject: [PATCH 1/4] draft version of new matlab API
- implement OpEnTcpOptimizer class
- some documentation
- old MATLAB codegen is now legacy
---
matlab/api/OpEnTcpOptimizer.m | 344 ++++++++++++++++++
matlab/api/createOpEnTcpOptimizer.m | 23 ++
.../@OpEnConstraints/OpEnConstraints.m | 0
.../@OpEnOptimizer/OpEnOptimizer.m | 0
.../OpEnOptimizerBuilder.m | 0
.../@OpEnOptimizerBuilder/build.m | 0
matlab/{ => legacy}/README.md | 0
matlab/{ => legacy}/examples/example_open_1.m | 0
.../{ => legacy}/examples/example_open_lv.m | 0
.../{ => legacy}/examples/example_open_nav.m | 0
.../helpers/casadi_generate_c_code.m | 0
matlab/{ => legacy}/helpers/rosenbrock.m | 0
matlab/{ => legacy}/matlab_open_root.m | 0
.../private/codegen_get_cache.txt | 0
matlab/{ => legacy}/private/codegen_head.txt | 0
.../{ => legacy}/private/codegen_main_2.txt | 0
.../{ => legacy}/private/codegen_main_3.txt | 0
.../private/codegen_main_fn_def.txt | 0
matlab/{ => legacy}/setup_open.m | 0
19 files changed, 367 insertions(+)
create mode 100644 matlab/api/OpEnTcpOptimizer.m
create mode 100644 matlab/api/createOpEnTcpOptimizer.m
rename matlab/{ => legacy}/@OpEnConstraints/OpEnConstraints.m (100%)
rename matlab/{ => legacy}/@OpEnOptimizer/OpEnOptimizer.m (100%)
rename matlab/{ => legacy}/@OpEnOptimizerBuilder/OpEnOptimizerBuilder.m (100%)
rename matlab/{ => legacy}/@OpEnOptimizerBuilder/build.m (100%)
rename matlab/{ => legacy}/README.md (100%)
rename matlab/{ => legacy}/examples/example_open_1.m (100%)
rename matlab/{ => legacy}/examples/example_open_lv.m (100%)
rename matlab/{ => legacy}/examples/example_open_nav.m (100%)
rename matlab/{ => legacy}/helpers/casadi_generate_c_code.m (100%)
rename matlab/{ => legacy}/helpers/rosenbrock.m (100%)
rename matlab/{ => legacy}/matlab_open_root.m (100%)
rename matlab/{ => legacy}/private/codegen_get_cache.txt (100%)
rename matlab/{ => legacy}/private/codegen_head.txt (100%)
rename matlab/{ => legacy}/private/codegen_main_2.txt (100%)
rename matlab/{ => legacy}/private/codegen_main_3.txt (100%)
rename matlab/{ => legacy}/private/codegen_main_fn_def.txt (100%)
rename matlab/{ => legacy}/setup_open.m (100%)
diff --git a/matlab/api/OpEnTcpOptimizer.m b/matlab/api/OpEnTcpOptimizer.m
new file mode 100644
index 00000000..92e1a709
--- /dev/null
+++ b/matlab/api/OpEnTcpOptimizer.m
@@ -0,0 +1,344 @@
+classdef OpEnTcpOptimizer < handle
+ %OPENTCPOPTIMIZER TCP client for Python-generated OpEn optimizers.
+ % CLIENT = OPENTCPOPTIMIZER(PORT) creates a client that connects to a
+ % TCP-enabled optimizer running on 127.0.0.1:PORT.
+ %
+ % CLIENT = OPENTCPOPTIMIZER(PORT, IP) connects to the optimizer at
+ % the specified IP address and TCP port.
+ %
+ % CLIENT = OPENTCPOPTIMIZER(IP, PORT) is also accepted for callers
+ % who prefer to provide the endpoint in IP/port order.
+ %
+ % CLIENT = OPENTCPOPTIMIZER(..., 'Timeout', T) sets the socket
+ % connect/read timeout in seconds.
+ %
+ % CLIENT = OPENTCPOPTIMIZER(..., 'MaxResponseBytes', N) limits the
+ % maximum number of bytes accepted from the optimizer.
+ %
+ % This interface is intended for optimizers generated in Python with
+ % the TCP interface enabled. The optimizer server must already be
+ % running; this class only communicates with it.
+ %
+ % Example:
+ % client = OpEnTcpOptimizer(3301);
+ % response = client.solve([2.0, 10.0]);
+ % if response.ok
+ % disp(response.solution);
+ % else
+ % error('OpEn:RemoteError', '%s', response.message);
+ % end
+
+ properties (SetAccess = private)
+ %IP IPv4 address or host name of the optimizer server.
+ ip
+
+ %PORT TCP port of the optimizer server.
+ port
+
+ %TIMEOUT Connect and read timeout in seconds.
+ timeout
+
+ %MAXRESPONSEBYTES Safety limit for incoming payload size.
+ maxResponseBytes
+ end
+
+ methods
+ function obj = OpEnTcpOptimizer(arg1, arg2, varargin)
+ %OPENTCPOPTIMIZER Construct a TCP client for a generated optimizer.
+ %
+ % OBJ = OPENTCPOPTIMIZER(PORT) uses 127.0.0.1.
+ % OBJ = OPENTCPOPTIMIZER(PORT, IP) uses the provided IP.
+ % OBJ = OPENTCPOPTIMIZER(IP, PORT) is also supported.
+ %
+ % Name-value pairs:
+ % 'Timeout' Socket timeout in seconds (default 10)
+ % 'MaxResponseBytes' Maximum response size in bytes
+ % (default 1048576)
+
+ if nargin < 1
+ error('OpEnTcpOptimizer:NotEnoughInputs', ...
+ 'You must provide at least a TCP port.');
+ end
+
+ if nargin < 2
+ arg2 = [];
+ end
+
+ [port, ip] = OpEnTcpOptimizer.normalizeEndpointArguments(arg1, arg2);
+
+ parser = inputParser();
+ parser.FunctionName = 'OpEnTcpOptimizer';
+ addRequired(parser, 'port', @OpEnTcpOptimizer.isValidPort);
+ addRequired(parser, 'ip', @OpEnTcpOptimizer.isTextScalar);
+ addParameter(parser, 'Timeout', 10, @OpEnTcpOptimizer.isValidTimeout);
+ addParameter(parser, 'MaxResponseBytes', 1048576, @OpEnTcpOptimizer.isValidMaxResponseBytes);
+ parse(parser, port, ip, varargin{:});
+
+ obj.port = double(parser.Results.port);
+ obj.ip = OpEnTcpOptimizer.textToChar(parser.Results.ip);
+ obj.timeout = double(parser.Results.Timeout);
+ obj.maxResponseBytes = double(parser.Results.MaxResponseBytes);
+ end
+
+ function response = ping(obj)
+ %PING Check whether the optimizer server is reachable.
+ % RESPONSE = PING(OBJ) sends {"Ping":1} and returns the
+ % decoded JSON response, typically a struct with field "Pong".
+ response = obj.sendRequest('{"Ping":1}', true);
+ end
+
+ function kill(obj)
+ %KILL Ask the optimizer server to stop gracefully.
+ % KILL(OBJ) sends {"Kill":1}. The server closes the
+ % connection without returning a JSON payload.
+ obj.sendRequest('{"Kill":1}', false);
+ end
+
+ function response = solve(obj, parameter, varargin)
+ %SOLVE Run the optimizer for the given parameter vector.
+ % RESPONSE = SOLVE(OBJ, PARAMETER) sends PARAMETER to the
+ % optimizer and returns a struct with field RESPONSE.ok.
+ %
+ % RESPONSE = SOLVE(..., 'InitialGuess', U0,
+ % 'InitialLagrangeMultipliers', Y0, 'InitialPenalty', C0)
+ % mirrors the TCP options supported by the generated server.
+ %
+ % On success, RESPONSE contains the solver fields returned by
+ % the server, plus the aliases:
+ % ok = true
+ % raw = original decoded JSON response
+ % f1_infeasibility = delta_y_norm_over_c
+ %
+ % On failure, RESPONSE contains:
+ % ok = false
+ % raw = original decoded JSON response
+ % code, message
+
+ parser = inputParser();
+ parser.FunctionName = 'OpEnTcpOptimizer.solve';
+ addRequired(parser, 'parameter', @OpEnTcpOptimizer.isVectorNumeric);
+ addParameter(parser, 'InitialGuess', [], @OpEnTcpOptimizer.isOptionalVectorNumeric);
+ addParameter(parser, 'InitialLagrangeMultipliers', [], @OpEnTcpOptimizer.isOptionalVectorNumeric);
+ addParameter(parser, 'InitialPenalty', [], @OpEnTcpOptimizer.isOptionalScalarNumeric);
+ parse(parser, parameter, varargin{:});
+
+ request = struct();
+ request.Run = struct();
+ request.Run.parameter = OpEnTcpOptimizer.toRowVector(parser.Results.parameter, 'parameter');
+
+ if ~isempty(parser.Results.InitialGuess)
+ request.Run.initial_guess = OpEnTcpOptimizer.toRowVector( ...
+ parser.Results.InitialGuess, 'InitialGuess');
+ end
+
+ if ~isempty(parser.Results.InitialLagrangeMultipliers)
+ request.Run.initial_lagrange_multipliers = OpEnTcpOptimizer.toRowVector( ...
+ parser.Results.InitialLagrangeMultipliers, 'InitialLagrangeMultipliers');
+ end
+
+ if ~isempty(parser.Results.InitialPenalty)
+ request.Run.initial_penalty = double(parser.Results.InitialPenalty);
+ end
+
+ rawResponse = obj.sendRequest(jsonencode(request), true);
+ response = OpEnTcpOptimizer.normalizeSolverResponse(rawResponse);
+ end
+
+ function response = call(obj, parameter, varargin)
+ %CALL Alias for SOLVE to match the Python TCP interface.
+ response = obj.solve(parameter, varargin{:});
+ end
+
+ function response = consume(obj, parameter, varargin)
+ %CONSUME Alias for SOLVE to ease migration from older MATLAB code.
+ response = obj.solve(parameter, varargin{:});
+ end
+ end
+
+ methods (Access = private)
+ function response = sendRequest(obj, requestText, expectReply)
+ %SENDREQUEST Send a JSON request and optionally decode a JSON reply.
+ %
+ % The generated Rust server reads until the client closes its
+ % write side. We therefore use Java sockets so we can call
+ % shutdownOutput() after transmitting the JSON payload.
+
+ socket = [];
+ cleanup = [];
+
+ try
+ socket = java.net.Socket();
+ timeoutMs = max(1, round(1000 * obj.timeout));
+ socket.connect(java.net.InetSocketAddress(obj.ip, obj.port), timeoutMs);
+ socket.setSoTimeout(timeoutMs);
+ cleanup = onCleanup(@() OpEnTcpOptimizer.closeSocketQuietly(socket));
+
+ outputStream = socket.getOutputStream();
+ requestBytes = int8(unicode2native(char(requestText), 'UTF-8'));
+ outputStream.write(requestBytes);
+ outputStream.flush();
+ socket.shutdownOutput();
+
+ if ~expectReply
+ return;
+ end
+
+ inputStream = socket.getInputStream();
+ responseBytes = obj.readFully(inputStream);
+ if isempty(responseBytes)
+ error('OpEnTcpOptimizer:EmptyResponse', ...
+ 'The optimizer server closed the connection without sending a response.');
+ end
+
+ responseText = native2unicode(responseBytes, 'UTF-8');
+ response = jsondecode(responseText);
+ catch err
+ % Ensure the socket is closed before rethrowing transport errors.
+ clear cleanup;
+ OpEnTcpOptimizer.closeSocketQuietly(socket);
+ rethrow(err);
+ end
+
+ clear cleanup;
+ end
+
+ function bytes = readFully(obj, inputStream)
+ %READFULLY Read the complete server reply until EOF.
+ %
+ % The server sends a single JSON document per connection and
+ % closes the connection afterwards, so EOF marks the end of
+ % the response payload.
+
+ byteStream = java.io.ByteArrayOutputStream();
+
+ while true
+ nextByte = inputStream.read();
+ if nextByte == -1
+ break;
+ end
+
+ byteStream.write(nextByte);
+ if byteStream.size() > obj.maxResponseBytes
+ error('OpEnTcpOptimizer:ResponseTooLarge', ...
+ 'The optimizer response exceeded %d bytes.', obj.maxResponseBytes);
+ end
+ end
+
+ rawBytes = uint8(mod(double(byteStream.toByteArray()), 256));
+ bytes = reshape(rawBytes, 1, []);
+ end
+ end
+
+ methods (Static, Access = private)
+ function response = normalizeSolverResponse(rawResponse)
+ %NORMALIZESOLVERRESPONSE Add MATLAB-friendly fields to server data.
+
+ response = rawResponse;
+ response.raw = rawResponse;
+
+ if isfield(rawResponse, 'type') && strcmp(rawResponse.type, 'Error')
+ response.ok = false;
+ return;
+ end
+
+ response.ok = true;
+ if isfield(rawResponse, 'delta_y_norm_over_c')
+ response.f1_infeasibility = rawResponse.delta_y_norm_over_c;
+ end
+ end
+
+ function vector = toRowVector(value, argumentName)
+ %TOROWVECTOR Validate a numeric vector and serialize it as a row.
+ if ~OpEnTcpOptimizer.isVectorNumeric(value)
+ error('OpEnTcpOptimizer:InvalidVector', ...
+ '%s must be a numeric vector.', argumentName);
+ end
+
+ vector = reshape(double(value), 1, []);
+ end
+
+ function closeSocketQuietly(socket)
+ %CLOSESOCKETQUIETLY Best-effort socket close for cleanup paths.
+ if isempty(socket)
+ return;
+ end
+
+ try
+ socket.close();
+ catch
+ % Ignore close errors during cleanup.
+ end
+ end
+
+ function tf = isValidPort(value)
+ %ISVALIDPORT Validate a TCP port number.
+ tf = isnumeric(value) && isscalar(value) && isfinite(value) ...
+ && value == fix(value) && value >= 1 && value <= 65535;
+ end
+
+ function tf = isValidTimeout(value)
+ %ISVALIDTIMEOUT Validate a positive timeout.
+ tf = isnumeric(value) && isscalar(value) && isfinite(value) && value > 0;
+ end
+
+ function tf = isValidMaxResponseBytes(value)
+ %ISVALIDMAXRESPONSEBYTES Validate the maximum response size.
+ tf = isnumeric(value) && isscalar(value) && isfinite(value) ...
+ && value == fix(value) && value > 0;
+ end
+
+ function tf = isTextScalar(value)
+ %ISTEXTSCALAR True for character vectors and string scalars.
+ tf = ischar(value) || (isstring(value) && isscalar(value));
+ end
+
+ function [port, ip] = normalizeEndpointArguments(arg1, arg2)
+ %NORMALIZEENDPOINTARGUMENTS Support both (port, ip) and (ip, port).
+ defaultIp = '127.0.0.1';
+
+ if isempty(arg2)
+ port = arg1;
+ ip = defaultIp;
+ return;
+ end
+
+ if OpEnTcpOptimizer.isValidPort(arg1) && OpEnTcpOptimizer.isTextScalar(arg2)
+ port = arg1;
+ ip = arg2;
+ return;
+ end
+
+ if OpEnTcpOptimizer.isTextScalar(arg1) && OpEnTcpOptimizer.isValidPort(arg2)
+ port = arg2;
+ ip = arg1;
+ return;
+ end
+
+ error('OpEnTcpOptimizer:InvalidEndpoint', ...
+ ['Specify the endpoint as (port), (port, ip), or (ip, port), ' ...
+ 'where port is an integer in [1, 65535].']);
+ end
+
+ function value = textToChar(value)
+ %TEXTTOCHAR Convert a MATLAB text scalar to a character vector.
+ if isstring(value)
+ value = char(value);
+ end
+ end
+
+ function tf = isVectorNumeric(value)
+ %ISVECTORNUMERIC True for finite numeric vectors.
+ tf = isnumeric(value) && isvector(value) && all(isfinite(value));
+ end
+
+ function tf = isOptionalVectorNumeric(value)
+ %ISOPTIONALVECTORNUMERIC True for [] or a numeric vector.
+ tf = isempty(value) || OpEnTcpOptimizer.isVectorNumeric(value);
+ end
+
+ function tf = isOptionalScalarNumeric(value)
+ %ISOPTIONALSCALARNUMERIC True for [] or a finite numeric scalar.
+ tf = isempty(value) || (isnumeric(value) && isscalar(value) && isfinite(value));
+ end
+ end
+end
diff --git a/matlab/api/createOpEnTcpOptimizer.m b/matlab/api/createOpEnTcpOptimizer.m
new file mode 100644
index 00000000..c90d0bbc
--- /dev/null
+++ b/matlab/api/createOpEnTcpOptimizer.m
@@ -0,0 +1,23 @@
+function client = createOpEnTcpOptimizer(arg1, arg2, varargin)
+%CREATEOPENTCPOPTIMIZER creates a MATLAB TCP client for an OpEn optimizer.
+% CLIENT = CREATEOPENTCPOPTIMIZER(PORT) connects to a TCP-enabled
+% generated optimizer on 127.0.0.1:PORT.
+%
+% CLIENT = CREATEOPENTCPOPTIMIZER(PORT, IP) connects to the specified
+% IP address and port.
+%
+% CLIENT = CREATEOPENTCPOPTIMIZER(IP, PORT) is also accepted.
+%
+% CLIENT = CREATEOPENTCPOPTIMIZER(..., Name, Value) forwards all
+% remaining name-value pairs to the OPENTCPOPTIMIZER constructor. See
+% "help OpEnTcpOptimizer" for the supported options and methods.
+%
+% This helper keeps the public API lightweight while the implementation
+% lives in the documented OpEnTcpOptimizer class.
+
+ if nargin < 2
+ arg2 = [];
+ end
+
+ client = OpEnTcpOptimizer(arg1, arg2, varargin{:});
+end
diff --git a/matlab/@OpEnConstraints/OpEnConstraints.m b/matlab/legacy/@OpEnConstraints/OpEnConstraints.m
similarity index 100%
rename from matlab/@OpEnConstraints/OpEnConstraints.m
rename to matlab/legacy/@OpEnConstraints/OpEnConstraints.m
diff --git a/matlab/@OpEnOptimizer/OpEnOptimizer.m b/matlab/legacy/@OpEnOptimizer/OpEnOptimizer.m
similarity index 100%
rename from matlab/@OpEnOptimizer/OpEnOptimizer.m
rename to matlab/legacy/@OpEnOptimizer/OpEnOptimizer.m
diff --git a/matlab/@OpEnOptimizerBuilder/OpEnOptimizerBuilder.m b/matlab/legacy/@OpEnOptimizerBuilder/OpEnOptimizerBuilder.m
similarity index 100%
rename from matlab/@OpEnOptimizerBuilder/OpEnOptimizerBuilder.m
rename to matlab/legacy/@OpEnOptimizerBuilder/OpEnOptimizerBuilder.m
diff --git a/matlab/@OpEnOptimizerBuilder/build.m b/matlab/legacy/@OpEnOptimizerBuilder/build.m
similarity index 100%
rename from matlab/@OpEnOptimizerBuilder/build.m
rename to matlab/legacy/@OpEnOptimizerBuilder/build.m
diff --git a/matlab/README.md b/matlab/legacy/README.md
similarity index 100%
rename from matlab/README.md
rename to matlab/legacy/README.md
diff --git a/matlab/examples/example_open_1.m b/matlab/legacy/examples/example_open_1.m
similarity index 100%
rename from matlab/examples/example_open_1.m
rename to matlab/legacy/examples/example_open_1.m
diff --git a/matlab/examples/example_open_lv.m b/matlab/legacy/examples/example_open_lv.m
similarity index 100%
rename from matlab/examples/example_open_lv.m
rename to matlab/legacy/examples/example_open_lv.m
diff --git a/matlab/examples/example_open_nav.m b/matlab/legacy/examples/example_open_nav.m
similarity index 100%
rename from matlab/examples/example_open_nav.m
rename to matlab/legacy/examples/example_open_nav.m
diff --git a/matlab/helpers/casadi_generate_c_code.m b/matlab/legacy/helpers/casadi_generate_c_code.m
similarity index 100%
rename from matlab/helpers/casadi_generate_c_code.m
rename to matlab/legacy/helpers/casadi_generate_c_code.m
diff --git a/matlab/helpers/rosenbrock.m b/matlab/legacy/helpers/rosenbrock.m
similarity index 100%
rename from matlab/helpers/rosenbrock.m
rename to matlab/legacy/helpers/rosenbrock.m
diff --git a/matlab/matlab_open_root.m b/matlab/legacy/matlab_open_root.m
similarity index 100%
rename from matlab/matlab_open_root.m
rename to matlab/legacy/matlab_open_root.m
diff --git a/matlab/private/codegen_get_cache.txt b/matlab/legacy/private/codegen_get_cache.txt
similarity index 100%
rename from matlab/private/codegen_get_cache.txt
rename to matlab/legacy/private/codegen_get_cache.txt
diff --git a/matlab/private/codegen_head.txt b/matlab/legacy/private/codegen_head.txt
similarity index 100%
rename from matlab/private/codegen_head.txt
rename to matlab/legacy/private/codegen_head.txt
diff --git a/matlab/private/codegen_main_2.txt b/matlab/legacy/private/codegen_main_2.txt
similarity index 100%
rename from matlab/private/codegen_main_2.txt
rename to matlab/legacy/private/codegen_main_2.txt
diff --git a/matlab/private/codegen_main_3.txt b/matlab/legacy/private/codegen_main_3.txt
similarity index 100%
rename from matlab/private/codegen_main_3.txt
rename to matlab/legacy/private/codegen_main_3.txt
diff --git a/matlab/private/codegen_main_fn_def.txt b/matlab/legacy/private/codegen_main_fn_def.txt
similarity index 100%
rename from matlab/private/codegen_main_fn_def.txt
rename to matlab/legacy/private/codegen_main_fn_def.txt
diff --git a/matlab/setup_open.m b/matlab/legacy/setup_open.m
similarity index 100%
rename from matlab/setup_open.m
rename to matlab/legacy/setup_open.m
From 2969e6a6778b1f88e74c71ee4abf3124b9bfc182 Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Thu, 26 Mar 2026 16:35:20 +0000
Subject: [PATCH 2/4] [ci skip] support for OCP solvers
---
matlab/api/OpEnTcpOptimizer.m | 756 ++++++++++++++++++++++++----
matlab/api/createOpEnTcpOptimizer.m | 17 +-
2 files changed, 662 insertions(+), 111 deletions(-)
diff --git a/matlab/api/OpEnTcpOptimizer.m b/matlab/api/OpEnTcpOptimizer.m
index 92e1a709..a0eceb5d 100644
--- a/matlab/api/OpEnTcpOptimizer.m
+++ b/matlab/api/OpEnTcpOptimizer.m
@@ -9,24 +9,33 @@
% CLIENT = OPENTCPOPTIMIZER(IP, PORT) is also accepted for callers
% who prefer to provide the endpoint in IP/port order.
%
- % CLIENT = OPENTCPOPTIMIZER(..., 'Timeout', T) sets the socket
- % connect/read timeout in seconds.
+ % CLIENT = OPENTCPOPTIMIZER(..., 'ManifestPath', MANIFESTPATH) loads
+ % an OCP optimizer manifest created by Python's ``ocp`` module. Once
+ % a manifest is loaded, the client also supports named-parameter
+ % calls such as:
%
- % CLIENT = OPENTCPOPTIMIZER(..., 'MaxResponseBytes', N) limits the
- % maximum number of bytes accepted from the optimizer.
+ % response = client.solve(x0=[1; 0], xref=[0; 0]);
+ %
+ % Name-value pairs:
+ % 'ManifestPath' Path to optimizer_manifest.json
+ % 'Timeout' Socket timeout in seconds (default 10)
+ % 'MaxResponseBytes' Maximum response size in bytes
+ % (default 1048576)
+ %
+ % If only a manifest path is provided, the constructor attempts to
+ % read ``optimizer.yml`` next to the manifest and use its TCP/IP
+ % endpoint automatically.
%
% This interface is intended for optimizers generated in Python with
% the TCP interface enabled. The optimizer server must already be
% running; this class only communicates with it.
%
- % Example:
+ % Examples:
% client = OpEnTcpOptimizer(3301);
% response = client.solve([2.0, 10.0]);
- % if response.ok
- % disp(response.solution);
- % else
- % error('OpEn:RemoteError', '%s', response.message);
- % end
+ %
+ % client = OpEnTcpOptimizer('ManifestPath', 'optimizer_manifest.json');
+ % response = client.solve(x0=[1.0, -1.0], xref=[0.0, 0.0]);
properties (SetAccess = private)
%IP IPv4 address or host name of the optimizer server.
@@ -40,44 +49,91 @@
%MAXRESPONSEBYTES Safety limit for incoming payload size.
maxResponseBytes
+
+ %MANIFESTPATH Absolute path to an optional OCP manifest.
+ manifestPath
+
+ %MANIFEST Decoded OCP manifest data.
+ manifest
end
methods
- function obj = OpEnTcpOptimizer(arg1, arg2, varargin)
+ function obj = OpEnTcpOptimizer(varargin)
%OPENTCPOPTIMIZER Construct a TCP client for a generated optimizer.
%
- % OBJ = OPENTCPOPTIMIZER(PORT) uses 127.0.0.1.
- % OBJ = OPENTCPOPTIMIZER(PORT, IP) uses the provided IP.
- % OBJ = OPENTCPOPTIMIZER(IP, PORT) is also supported.
- %
- % Name-value pairs:
- % 'Timeout' Socket timeout in seconds (default 10)
- % 'MaxResponseBytes' Maximum response size in bytes
- % (default 1048576)
+ % Supported call patterns:
+ % OpEnTcpOptimizer(port)
+ % OpEnTcpOptimizer(port, ip)
+ % OpEnTcpOptimizer(ip, port)
+ % OpEnTcpOptimizer(..., 'ManifestPath', path)
+ % OpEnTcpOptimizer('ManifestPath', path)
+
+ obj.manifestPath = '';
+ obj.manifest = [];
+
+ [endpointArgs, options] = OpEnTcpOptimizer.parseConstructorInputs(varargin);
- if nargin < 1
- error('OpEnTcpOptimizer:NotEnoughInputs', ...
- 'You must provide at least a TCP port.');
+ if ~isempty(options.ManifestPath)
+ obj.loadManifest(options.ManifestPath);
end
- if nargin < 2
- arg2 = [];
+ [port, ip] = obj.resolveEndpoint(endpointArgs);
+
+ obj.port = double(port);
+ obj.ip = OpEnTcpOptimizer.textToChar(ip);
+ obj.timeout = double(options.Timeout);
+ obj.maxResponseBytes = double(options.MaxResponseBytes);
+ end
+
+ function obj = loadManifest(obj, manifestPath)
+ %LOADMANIFEST Load an OCP optimizer manifest.
+ % OBJ = LOADMANIFEST(OBJ, MANIFESTPATH) loads an
+ % ``optimizer_manifest.json`` file created by the Python OCP
+ % module. After loading the manifest, the client accepts
+ % named-parameter calls such as ``solve(x0=..., xref=...)``.
+
+ if ~OpEnTcpOptimizer.isTextScalar(manifestPath)
+ error('OpEnTcpOptimizer:InvalidManifestPath', ...
+ 'ManifestPath must be a character vector or string scalar.');
end
+ manifestPath = OpEnTcpOptimizer.textToChar(manifestPath);
+ manifestPath = OpEnTcpOptimizer.validateManifestPath(manifestPath);
+ manifestText = fileread(manifestPath);
+ manifestData = jsondecode(manifestText);
+
+ if ~isstruct(manifestData)
+ error('OpEnTcpOptimizer:InvalidManifest', ...
+ 'The manifest must decode to a MATLAB struct.');
+ end
+
+ if ~isfield(manifestData, 'parameters')
+ error('OpEnTcpOptimizer:InvalidManifest', ...
+ 'The manifest does not contain a "parameters" field.');
+ end
+
+ OpEnTcpOptimizer.validateManifestParameters(manifestData.parameters);
- [port, ip] = OpEnTcpOptimizer.normalizeEndpointArguments(arg1, arg2);
+ obj.manifestPath = manifestPath;
+ obj.manifest = manifestData;
+ end
+
+ function tf = hasManifest(obj)
+ %HASMANIFEST True if an OCP manifest has been loaded.
+ tf = ~isempty(obj.manifest);
+ end
- parser = inputParser();
- parser.FunctionName = 'OpEnTcpOptimizer';
- addRequired(parser, 'port', @OpEnTcpOptimizer.isValidPort);
- addRequired(parser, 'ip', @OpEnTcpOptimizer.isTextScalar);
- addParameter(parser, 'Timeout', 10, @OpEnTcpOptimizer.isValidTimeout);
- addParameter(parser, 'MaxResponseBytes', 1048576, @OpEnTcpOptimizer.isValidMaxResponseBytes);
- parse(parser, port, ip, varargin{:});
+ function names = parameterNames(obj)
+ %PARAMETERNAMES Return the ordered OCP parameter names.
+ if ~obj.hasManifest()
+ names = {};
+ return;
+ end
- obj.port = double(parser.Results.port);
- obj.ip = OpEnTcpOptimizer.textToChar(parser.Results.ip);
- obj.timeout = double(parser.Results.Timeout);
- obj.maxResponseBytes = double(parser.Results.MaxResponseBytes);
+ definitions = OpEnTcpOptimizer.manifestParametersAsCell(obj.manifest.parameters);
+ names = cell(size(definitions));
+ for i = 1:numel(definitions)
+ names{i} = definitions{i}.name;
+ end
end
function response = ping(obj)
@@ -94,68 +150,289 @@ function kill(obj)
obj.sendRequest('{"Kill":1}', false);
end
- function response = solve(obj, parameter, varargin)
- %SOLVE Run the optimizer for the given parameter vector.
- % RESPONSE = SOLVE(OBJ, PARAMETER) sends PARAMETER to the
- % optimizer and returns a struct with field RESPONSE.ok.
+ function response = solve(obj, varargin)
+ %SOLVE Run a parametric or OCP optimizer over TCP.
+ % RESPONSE = SOLVE(OBJ, P) sends the flat parameter vector P
+ % to a standard parametric optimizer.
%
- % RESPONSE = SOLVE(..., 'InitialGuess', U0,
- % 'InitialLagrangeMultipliers', Y0, 'InitialPenalty', C0)
- % mirrors the TCP options supported by the generated server.
+ % RESPONSE = SOLVE(OBJ, x0=..., xref=..., ...) packs the named
+ % parameter blocks declared in the loaded OCP manifest and
+ % sends the resulting flat parameter vector to the solver.
+ %
+ % In both modes, the optional solver warm-start arguments are:
+ % InitialGuess
+ % InitialLagrangeMultipliers
+ % InitialPenalty
%
% On success, RESPONSE contains the solver fields returned by
- % the server, plus the aliases:
+ % the server, plus:
% ok = true
% raw = original decoded JSON response
% f1_infeasibility = delta_y_norm_over_c
%
- % On failure, RESPONSE contains:
- % ok = false
- % raw = original decoded JSON response
- % code, message
+ % For OCP solves, RESPONSE also contains:
+ % packed_parameter = flat parameter vector sent to server
+ % inputs = stage-wise control inputs, when available
+ % states = state trajectory for multiple shooting OCPs
+
+ [parameterVector, solverOptions, solveMode] = obj.prepareSolveInputs(varargin);
+ rawResponse = obj.runSolveRequest(parameterVector, solverOptions);
+ response = OpEnTcpOptimizer.normalizeSolverResponse(rawResponse);
+
+ if strcmp(solveMode, 'ocp')
+ response.packed_parameter = parameterVector;
+ if response.ok
+ response = obj.enrichOcpResponse(response, parameterVector);
+ end
+ end
+ end
+
+ function response = call(obj, varargin)
+ %CALL Alias for SOLVE to match the Python TCP interface.
+ response = obj.solve(varargin{:});
+ end
+
+ function response = consume(obj, varargin)
+ %CONSUME Alias for SOLVE to ease migration from older MATLAB code.
+ response = obj.solve(varargin{:});
+ end
+ end
+
+ methods (Access = private)
+ function [parameterVector, solverOptions, solveMode] = prepareSolveInputs(obj, inputArgs)
+ %PREPARESOLVEINPUTS Parse parametric or OCP solve inputs.
+
+ if isempty(inputArgs)
+ error('OpEnTcpOptimizer:MissingSolveArguments', ...
+ 'Provide either a flat parameter vector or named OCP parameters.');
+ end
+
+ if OpEnTcpOptimizer.isVectorNumeric(inputArgs{1})
+ solveMode = 'parametric';
+ parameterVector = OpEnTcpOptimizer.toRowVector(inputArgs{1}, 'parameter');
+ solverOptions = OpEnTcpOptimizer.parseSolverOptions(inputArgs(2:end));
+ return;
+ end
+
+ if ~obj.hasManifest()
+ error('OpEnTcpOptimizer:ManifestRequired', ...
+ ['Named parameter solves require an OCP manifest. Load one with ' ...
+ 'loadManifest(...) or the constructor option ''ManifestPath''.']);
+ end
+
+ [parameterVector, solverOptions] = obj.packOcpParameters(inputArgs);
+ solveMode = 'ocp';
+ end
+
+ function [parameterVector, solverOptions] = packOcpParameters(obj, inputArgs)
+ %PACKOCPPARAMETERS Pack named OCP parameters using the manifest.
+
+ pairs = OpEnTcpOptimizer.normalizeNameValuePairs(inputArgs, 'OpEnTcpOptimizer.solve');
+ solverOptions = OpEnTcpOptimizer.emptySolverOptions();
+ providedValues = containers.Map('KeyType', 'char', 'ValueType', 'any');
+
+ for i = 1:size(pairs, 1)
+ name = pairs{i, 1};
+ value = pairs{i, 2};
+ lowerName = lower(name);
+
+ switch lowerName
+ case 'initialguess'
+ solverOptions.InitialGuess = value;
+ case 'initiallagrangemultipliers'
+ solverOptions.InitialLagrangeMultipliers = value;
+ case 'initialpenalty'
+ solverOptions.InitialPenalty = value;
+ otherwise
+ if isKey(providedValues, name)
+ error('OpEnTcpOptimizer:DuplicateParameter', ...
+ 'Parameter "%s" was provided more than once.', name);
+ end
+ providedValues(name) = value;
+ end
+ end
+
+ solverOptions = OpEnTcpOptimizer.validateSolverOptions(solverOptions);
+
+ definitions = OpEnTcpOptimizer.manifestParametersAsCell(obj.manifest.parameters);
+ parameterVector = [];
+ missing = {};
+
+ for i = 1:numel(definitions)
+ definition = definitions{i};
+ if isKey(providedValues, definition.name)
+ value = providedValues(definition.name);
+ remove(providedValues, definition.name);
+ else
+ value = definition.default;
+ end
+
+ if isempty(value)
+ missing{end + 1} = definition.name; %#ok
+ continue;
+ end
+
+ parameterVector = [parameterVector, ... %#ok
+ OpEnTcpOptimizer.normalizeParameterBlock( ...
+ value, definition.size, definition.name)];
+ end
+
+ if ~isempty(missing)
+ error('OpEnTcpOptimizer:MissingOcpParameters', ...
+ 'Missing values for parameters: %s.', strjoin(missing, ', '));
+ end
+
+ remainingNames = sort(keys(providedValues));
+ if ~isempty(remainingNames)
+ error('OpEnTcpOptimizer:UnknownOcpParameters', ...
+ 'Unknown OCP parameter(s): %s.', strjoin(remainingNames, ', '));
+ end
+ end
- parser = inputParser();
- parser.FunctionName = 'OpEnTcpOptimizer.solve';
- addRequired(parser, 'parameter', @OpEnTcpOptimizer.isVectorNumeric);
- addParameter(parser, 'InitialGuess', [], @OpEnTcpOptimizer.isOptionalVectorNumeric);
- addParameter(parser, 'InitialLagrangeMultipliers', [], @OpEnTcpOptimizer.isOptionalVectorNumeric);
- addParameter(parser, 'InitialPenalty', [], @OpEnTcpOptimizer.isOptionalScalarNumeric);
- parse(parser, parameter, varargin{:});
+ function response = enrichOcpResponse(obj, response, packedParameters)
+ %ENRICHOCPRESPONSE Add OCP-oriented views to a successful solve.
+
+ response.inputs = obj.extractOcpInputs(response.solution);
+
+ if strcmp(obj.manifest.shooting, 'multiple')
+ response.states = obj.extractMultipleShootingStates( ...
+ response.solution, packedParameters);
+ end
+ end
+
+ function inputs = extractOcpInputs(obj, flatSolution)
+ %EXTRACTOCPINPUTS Extract stage-wise input blocks from the solution.
+
+ flatSolution = OpEnTcpOptimizer.toRowVector(flatSolution, 'solution');
+
+ if strcmp(obj.manifest.shooting, 'single')
+ nu = double(obj.manifest.nu);
+ horizon = double(obj.manifest.horizon);
+ inputs = cell(1, horizon);
+
+ for stageIdx = 1:horizon
+ startIdx = (stageIdx - 1) * nu + 1;
+ stopIdx = stageIdx * nu;
+ inputs{stageIdx} = flatSolution(startIdx:stopIdx);
+ end
+ return;
+ end
+
+ sliceMatrix = obj.manifest.input_slices;
+ inputs = cell(1, size(sliceMatrix, 1));
+ for i = 1:size(sliceMatrix, 1)
+ startIdx = sliceMatrix(i, 1) + 1;
+ stopIdx = sliceMatrix(i, 2);
+ inputs{i} = flatSolution(startIdx:stopIdx);
+ end
+ end
+
+ function states = extractMultipleShootingStates(obj, flatSolution, packedParameters)
+ %EXTRACTMULTIPLESHOOTINGSTATES Extract the state trajectory.
+ %
+ % For multiple shooting OCPs the manifest contains the state
+ % slices directly, so no extra CasADi dependency is needed in
+ % MATLAB to reconstruct the state trajectory.
+
+ flatSolution = OpEnTcpOptimizer.toRowVector(flatSolution, 'solution');
+ packedParameters = OpEnTcpOptimizer.toRowVector(packedParameters, 'packedParameters');
+
+ stateSlices = obj.manifest.state_slices;
+ states = cell(1, size(stateSlices, 1) + 1);
+ states{1} = obj.extractParameterByName(packedParameters, 'x0');
+
+ for i = 1:size(stateSlices, 1)
+ startIdx = stateSlices(i, 1) + 1;
+ stopIdx = stateSlices(i, 2);
+ states{i + 1} = flatSolution(startIdx:stopIdx);
+ end
+ end
+
+ function value = extractParameterByName(obj, packedParameters, parameterName)
+ %EXTRACTPARAMETERBYNAME Extract one named parameter block.
+
+ definitions = OpEnTcpOptimizer.manifestParametersAsCell(obj.manifest.parameters);
+ offset = 0;
+
+ for i = 1:numel(definitions)
+ definition = definitions{i};
+ nextOffset = offset + definition.size;
+ if strcmp(definition.name, parameterName)
+ value = packedParameters(offset + 1:nextOffset);
+ return;
+ end
+ offset = nextOffset;
+ end
+
+ error('OpEnTcpOptimizer:MissingManifestParameter', ...
+ 'The manifest does not define a parameter named "%s".', parameterName);
+ end
+
+ function rawResponse = runSolveRequest(obj, parameterVector, solverOptions)
+ %RUNSOLVEREQUEST Serialize and send a solver execution request.
request = struct();
request.Run = struct();
- request.Run.parameter = OpEnTcpOptimizer.toRowVector(parser.Results.parameter, 'parameter');
+ request.Run.parameter = OpEnTcpOptimizer.toRowVector(parameterVector, 'parameter');
- if ~isempty(parser.Results.InitialGuess)
+ if ~isempty(solverOptions.InitialGuess)
request.Run.initial_guess = OpEnTcpOptimizer.toRowVector( ...
- parser.Results.InitialGuess, 'InitialGuess');
+ solverOptions.InitialGuess, 'InitialGuess');
end
- if ~isempty(parser.Results.InitialLagrangeMultipliers)
+ if ~isempty(solverOptions.InitialLagrangeMultipliers)
request.Run.initial_lagrange_multipliers = OpEnTcpOptimizer.toRowVector( ...
- parser.Results.InitialLagrangeMultipliers, 'InitialLagrangeMultipliers');
+ solverOptions.InitialLagrangeMultipliers, 'InitialLagrangeMultipliers');
end
- if ~isempty(parser.Results.InitialPenalty)
- request.Run.initial_penalty = double(parser.Results.InitialPenalty);
+ if ~isempty(solverOptions.InitialPenalty)
+ request.Run.initial_penalty = double(solverOptions.InitialPenalty);
end
rawResponse = obj.sendRequest(jsonencode(request), true);
- response = OpEnTcpOptimizer.normalizeSolverResponse(rawResponse);
end
- function response = call(obj, parameter, varargin)
- %CALL Alias for SOLVE to match the Python TCP interface.
- response = obj.solve(parameter, varargin{:});
- end
+ function [port, ip] = resolveEndpoint(obj, endpointArgs)
+ %RESOLVEENDPOINT Resolve the TCP endpoint from inputs or manifest.
- function response = consume(obj, parameter, varargin)
- %CONSUME Alias for SOLVE to ease migration from older MATLAB code.
- response = obj.solve(parameter, varargin{:});
+ if isempty(endpointArgs)
+ if ~obj.hasManifest()
+ error('OpEnTcpOptimizer:MissingEndpoint', ...
+ 'Provide a TCP endpoint or a manifest with a matching optimizer.yml file.');
+ end
+
+ tcpDefaults = OpEnTcpOptimizer.readTcpDefaultsFromManifest(obj.manifestPath);
+ if isempty(tcpDefaults)
+ error('OpEnTcpOptimizer:MissingEndpoint', ...
+ ['No TCP endpoint was provided and no TCP settings could be read from ' ...
+ 'optimizer.yml next to the manifest.']);
+ end
+
+ port = tcpDefaults.port;
+ ip = tcpDefaults.ip;
+ return;
+ end
+
+ if numel(endpointArgs) == 1
+ if ~OpEnTcpOptimizer.isValidPort(endpointArgs{1})
+ error('OpEnTcpOptimizer:InvalidEndpoint', ...
+ 'A single endpoint argument must be a TCP port.');
+ end
+ port = endpointArgs{1};
+ ip = '127.0.0.1';
+ return;
+ end
+
+ if numel(endpointArgs) == 2
+ [port, ip] = OpEnTcpOptimizer.normalizeEndpointArguments( ...
+ endpointArgs{1}, endpointArgs{2});
+ return;
+ end
+
+ error('OpEnTcpOptimizer:InvalidEndpoint', ...
+ 'Specify the endpoint as (port), (port, ip), or (ip, port).');
end
- end
- methods (Access = private)
function response = sendRequest(obj, requestText, expectReply)
%SENDREQUEST Send a JSON request and optionally decode a JSON reply.
%
@@ -230,6 +507,170 @@ function kill(obj)
end
methods (Static, Access = private)
+ function [endpointArgs, options] = parseConstructorInputs(inputArgs)
+ %PARSECONSTRUCTORINPUTS Split constructor endpoint and options.
+
+ endpointArgs = {};
+ options = struct( ...
+ 'ManifestPath', '', ...
+ 'Timeout', 10, ...
+ 'MaxResponseBytes', 1048576);
+
+ idx = 1;
+ while idx <= numel(inputArgs)
+ token = inputArgs{idx};
+ if OpEnTcpOptimizer.isRecognizedConstructorOption(token)
+ if idx == numel(inputArgs)
+ error('OpEnTcpOptimizer:InvalidConstructorInput', ...
+ 'Missing value for option "%s".', OpEnTcpOptimizer.textToChar(token));
+ end
+
+ name = lower(OpEnTcpOptimizer.textToChar(token));
+ value = inputArgs{idx + 1};
+ switch name
+ case 'manifestpath'
+ if ~OpEnTcpOptimizer.isTextScalar(value)
+ error('OpEnTcpOptimizer:InvalidManifestPath', ...
+ 'ManifestPath must be a character vector or string scalar.');
+ end
+ options.ManifestPath = OpEnTcpOptimizer.textToChar(value);
+ case 'timeout'
+ options.Timeout = value;
+ case 'maxresponsebytes'
+ options.MaxResponseBytes = value;
+ end
+ idx = idx + 2;
+ else
+ endpointArgs{end + 1} = token; %#ok
+ idx = idx + 1;
+ end
+ end
+
+ if numel(endpointArgs) == 1 && OpEnTcpOptimizer.isManifestPathToken(endpointArgs{1})
+ options.ManifestPath = OpEnTcpOptimizer.textToChar(endpointArgs{1});
+ endpointArgs = {};
+ end
+
+ if ~(OpEnTcpOptimizer.isValidTimeout(options.Timeout))
+ error('OpEnTcpOptimizer:InvalidTimeout', ...
+ 'Timeout must be a positive scalar.');
+ end
+
+ if ~(OpEnTcpOptimizer.isValidMaxResponseBytes(options.MaxResponseBytes))
+ error('OpEnTcpOptimizer:InvalidMaxResponseBytes', ...
+ 'MaxResponseBytes must be a positive integer.');
+ end
+ end
+
+ function tf = isRecognizedConstructorOption(token)
+ %ISRECOGNIZEDCONSTRUCTOROPTION True for constructor option names.
+ tf = OpEnTcpOptimizer.isTextScalar(token) && any(strcmpi( ...
+ OpEnTcpOptimizer.textToChar(token), {'ManifestPath', 'Timeout', 'MaxResponseBytes'}));
+ end
+
+ function tf = isManifestPathToken(token)
+ %ISMANIFESTPATHTOKEN Heuristic for a positional manifest path.
+ if ~OpEnTcpOptimizer.isTextScalar(token)
+ tf = false;
+ return;
+ end
+
+ token = OpEnTcpOptimizer.textToChar(token);
+ [~, ~, ext] = fileparts(token);
+ tf = strcmpi(ext, '.json') && isfile(token);
+ end
+
+ function manifestPath = validateManifestPath(manifestPath)
+ %VALIDATEMANIFESTPATH Validate and absolutize a manifest path.
+ if ~isfile(manifestPath)
+ error('OpEnTcpOptimizer:ManifestNotFound', ...
+ 'Manifest file not found: %s', manifestPath);
+ end
+
+ [folder, name, ext] = fileparts(manifestPath);
+ if ~strcmpi(ext, '.json')
+ error('OpEnTcpOptimizer:InvalidManifestPath', ...
+ 'The manifest path must point to a JSON file.');
+ end
+
+ manifestPath = fullfile(folder, [name, ext]);
+ end
+
+ function validateManifestParameters(parameters)
+ %VALIDATEMANIFESTPARAMETERS Validate manifest parameter entries.
+ definitions = OpEnTcpOptimizer.manifestParametersAsCell(parameters);
+ for i = 1:numel(definitions)
+ definition = definitions{i};
+ if ~isfield(definition, 'name') || ~isfield(definition, 'size')
+ error('OpEnTcpOptimizer:InvalidManifest', ...
+ 'Each manifest parameter needs "name" and "size" fields.');
+ end
+ if ~ischar(definition.name) && ~isstring(definition.name)
+ error('OpEnTcpOptimizer:InvalidManifest', ...
+ 'Manifest parameter names must be text values.');
+ end
+ if ~OpEnTcpOptimizer.isValidPositiveInteger(definition.size)
+ error('OpEnTcpOptimizer:InvalidManifest', ...
+ 'Manifest parameter sizes must be positive integers.');
+ end
+ end
+ end
+
+ function defaults = readTcpDefaultsFromManifest(manifestPath)
+ %READTCPDEFAULTSFROMMANIFEST Read TCP defaults from optimizer.yml.
+ defaults = [];
+ optimizerDir = fileparts(manifestPath);
+ yamlPath = fullfile(optimizerDir, 'optimizer.yml');
+
+ if ~isfile(yamlPath)
+ return;
+ end
+
+ defaults = OpEnTcpOptimizer.parseOptimizerYaml(yamlPath);
+ end
+
+ function defaults = parseOptimizerYaml(yamlPath)
+ %PARSEOPTIMIZERYAML Read the tcp.ip and tcp.port fields.
+ defaults = [];
+ yamlText = fileread(yamlPath);
+ lines = regexp(yamlText, '\r\n|\n|\r', 'split');
+
+ inTcpBlock = false;
+ ip = '';
+ port = [];
+
+ for i = 1:numel(lines)
+ line = lines{i};
+ trimmed = strtrim(line);
+
+ if isempty(trimmed)
+ continue;
+ end
+
+ if strcmp(trimmed, 'tcp:')
+ inTcpBlock = true;
+ continue;
+ end
+
+ if inTcpBlock && ~isempty(line) && ~isspace(line(1))
+ break;
+ end
+
+ if inTcpBlock
+ if startsWith(trimmed, 'ip:')
+ ip = strtrim(extractAfter(trimmed, 3));
+ elseif startsWith(trimmed, 'port:')
+ portText = strtrim(extractAfter(trimmed, 5));
+ port = str2double(portText);
+ end
+ end
+ end
+
+ if ~isempty(ip) && OpEnTcpOptimizer.isValidPort(port)
+ defaults = struct('ip', ip, 'port', port);
+ end
+ end
+
function response = normalizeSolverResponse(rawResponse)
%NORMALIZESOLVERRESPONSE Add MATLAB-friendly fields to server data.
@@ -247,6 +688,122 @@ function kill(obj)
end
end
+ function solverOptions = parseSolverOptions(inputArgs)
+ %PARSESOLVEROPTIONS Parse warm-start related name-value pairs.
+ pairs = OpEnTcpOptimizer.normalizeNameValuePairs(inputArgs, 'OpEnTcpOptimizer.solve');
+ solverOptions = OpEnTcpOptimizer.emptySolverOptions();
+
+ for i = 1:size(pairs, 1)
+ name = lower(pairs{i, 1});
+ value = pairs{i, 2};
+
+ switch name
+ case 'initialguess'
+ solverOptions.InitialGuess = value;
+ case 'initiallagrangemultipliers'
+ solverOptions.InitialLagrangeMultipliers = value;
+ case 'initialpenalty'
+ solverOptions.InitialPenalty = value;
+ otherwise
+ error('OpEnTcpOptimizer:UnknownSolveOption', ...
+ 'Unknown solve option "%s".', pairs{i, 1});
+ end
+ end
+
+ solverOptions = OpEnTcpOptimizer.validateSolverOptions(solverOptions);
+ end
+
+ function solverOptions = validateSolverOptions(solverOptions)
+ %VALIDATESOLVEROPTIONS Validate optional warm-start inputs.
+ if ~OpEnTcpOptimizer.isOptionalVectorNumeric(solverOptions.InitialGuess)
+ error('OpEnTcpOptimizer:InvalidInitialGuess', ...
+ 'InitialGuess must be a numeric vector or [].');
+ end
+
+ if ~OpEnTcpOptimizer.isOptionalVectorNumeric(solverOptions.InitialLagrangeMultipliers)
+ error('OpEnTcpOptimizer:InvalidInitialLagrangeMultipliers', ...
+ 'InitialLagrangeMultipliers must be a numeric vector or [].');
+ end
+
+ if ~OpEnTcpOptimizer.isOptionalScalarNumeric(solverOptions.InitialPenalty)
+ error('OpEnTcpOptimizer:InvalidInitialPenalty', ...
+ 'InitialPenalty must be a numeric scalar or [].');
+ end
+ end
+
+ function solverOptions = emptySolverOptions()
+ %EMPTYSOLVEROPTIONS Return the default solve option bundle.
+ solverOptions = struct( ...
+ 'InitialGuess', [], ...
+ 'InitialLagrangeMultipliers', [], ...
+ 'InitialPenalty', []);
+ end
+
+ function pairs = normalizeNameValuePairs(inputArgs, functionName)
+ %NORMALIZENAMEVALUEPAIRS Validate and normalize name-value pairs.
+ if isempty(inputArgs)
+ pairs = cell(0, 2);
+ return;
+ end
+
+ if mod(numel(inputArgs), 2) ~= 0
+ error('OpEnTcpOptimizer:InvalidNameValueInput', ...
+ '%s expects name-value arguments in pairs.', functionName);
+ end
+
+ pairs = cell(numel(inputArgs) / 2, 2);
+ pairIdx = 1;
+ for i = 1:2:numel(inputArgs)
+ name = inputArgs{i};
+ if ~OpEnTcpOptimizer.isTextScalar(name)
+ error('OpEnTcpOptimizer:InvalidNameValueInput', ...
+ 'Expected a text parameter name at argument position %d.', i);
+ end
+
+ pairs{pairIdx, 1} = OpEnTcpOptimizer.textToChar(name);
+ pairs{pairIdx, 2} = inputArgs{i + 1};
+ pairIdx = pairIdx + 1;
+ end
+ end
+
+ function blocks = manifestParametersAsCell(parameters)
+ %MANIFESTPARAMETERSASCELL Normalize decoded parameter definitions.
+ if isempty(parameters)
+ blocks = {};
+ return;
+ end
+
+ if isstruct(parameters)
+ blocks = cell(1, numel(parameters));
+ for i = 1:numel(parameters)
+ blocks{i} = parameters(i);
+ end
+ return;
+ end
+
+ error('OpEnTcpOptimizer:InvalidManifest', ...
+ 'Manifest parameters must decode to a struct array.');
+ end
+
+ function vector = normalizeParameterBlock(value, expectedSize, parameterName)
+ %NORMALIZEPARAMETERBLOCK Normalize one OCP parameter block.
+ if expectedSize == 1 && isnumeric(value) && isscalar(value) && isfinite(value)
+ vector = double(value);
+ return;
+ end
+
+ if ~OpEnTcpOptimizer.isVectorNumeric(value)
+ error('OpEnTcpOptimizer:InvalidOcpParameter', ...
+ 'Parameter "%s" must be a numeric vector.', parameterName);
+ end
+
+ vector = reshape(double(value), 1, []);
+ if numel(vector) ~= double(expectedSize)
+ error('OpEnTcpOptimizer:InvalidOcpParameterDimension', ...
+ 'Parameter "%s" must have length %d.', parameterName, double(expectedSize));
+ end
+ end
+
function vector = toRowVector(value, argumentName)
%TOROWVECTOR Validate a numeric vector and serialize it as a row.
if ~OpEnTcpOptimizer.isVectorNumeric(value)
@@ -270,10 +827,29 @@ function closeSocketQuietly(socket)
end
end
+ function [port, ip] = normalizeEndpointArguments(arg1, arg2)
+ %NORMALIZEENDPOINTARGUMENTS Support both (port, ip) and (ip, port).
+ if OpEnTcpOptimizer.isValidPort(arg1) && OpEnTcpOptimizer.isTextScalar(arg2)
+ port = arg1;
+ ip = arg2;
+ return;
+ end
+
+ if OpEnTcpOptimizer.isTextScalar(arg1) && OpEnTcpOptimizer.isValidPort(arg2)
+ port = arg2;
+ ip = arg1;
+ return;
+ end
+
+ error('OpEnTcpOptimizer:InvalidEndpoint', ...
+ ['Specify the endpoint as (port), (port, ip), or (ip, port), ' ...
+ 'where port is an integer in [1, 65535].']);
+ end
+
function tf = isValidPort(value)
%ISVALIDPORT Validate a TCP port number.
- tf = isnumeric(value) && isscalar(value) && isfinite(value) ...
- && value == fix(value) && value >= 1 && value <= 65535;
+ tf = OpEnTcpOptimizer.isValidPositiveInteger(value) ...
+ && double(value) >= 1 && double(value) <= 65535;
end
function tf = isValidTimeout(value)
@@ -283,6 +859,11 @@ function closeSocketQuietly(socket)
function tf = isValidMaxResponseBytes(value)
%ISVALIDMAXRESPONSEBYTES Validate the maximum response size.
+ tf = OpEnTcpOptimizer.isValidPositiveInteger(value);
+ end
+
+ function tf = isValidPositiveInteger(value)
+ %ISVALIDPOSITIVEINTEGER Validate a positive integer scalar.
tf = isnumeric(value) && isscalar(value) && isfinite(value) ...
&& value == fix(value) && value > 0;
end
@@ -292,33 +873,6 @@ function closeSocketQuietly(socket)
tf = ischar(value) || (isstring(value) && isscalar(value));
end
- function [port, ip] = normalizeEndpointArguments(arg1, arg2)
- %NORMALIZEENDPOINTARGUMENTS Support both (port, ip) and (ip, port).
- defaultIp = '127.0.0.1';
-
- if isempty(arg2)
- port = arg1;
- ip = defaultIp;
- return;
- end
-
- if OpEnTcpOptimizer.isValidPort(arg1) && OpEnTcpOptimizer.isTextScalar(arg2)
- port = arg1;
- ip = arg2;
- return;
- end
-
- if OpEnTcpOptimizer.isTextScalar(arg1) && OpEnTcpOptimizer.isValidPort(arg2)
- port = arg2;
- ip = arg1;
- return;
- end
-
- error('OpEnTcpOptimizer:InvalidEndpoint', ...
- ['Specify the endpoint as (port), (port, ip), or (ip, port), ' ...
- 'where port is an integer in [1, 65535].']);
- end
-
function value = textToChar(value)
%TEXTTOCHAR Convert a MATLAB text scalar to a character vector.
if isstring(value)
diff --git a/matlab/api/createOpEnTcpOptimizer.m b/matlab/api/createOpEnTcpOptimizer.m
index c90d0bbc..fd1fb57a 100644
--- a/matlab/api/createOpEnTcpOptimizer.m
+++ b/matlab/api/createOpEnTcpOptimizer.m
@@ -1,5 +1,5 @@
-function client = createOpEnTcpOptimizer(arg1, arg2, varargin)
-%CREATEOPENTCPOPTIMIZER creates a MATLAB TCP client for an OpEn optimizer.
+function client = createOpEnTcpOptimizer(varargin)
+%CREATEOPENTCPOPTIMIZER Create a MATLAB TCP client for an OpEn optimizer.
% CLIENT = CREATEOPENTCPOPTIMIZER(PORT) connects to a TCP-enabled
% generated optimizer on 127.0.0.1:PORT.
%
@@ -8,16 +8,13 @@
%
% CLIENT = CREATEOPENTCPOPTIMIZER(IP, PORT) is also accepted.
%
+% CLIENT = CREATEOPENTCPOPTIMIZER('ManifestPath', MANIFESTPATH) creates
+% a manifest-aware OCP TCP client and tries to read the endpoint from the
+% sibling ``optimizer.yml`` file.
+%
% CLIENT = CREATEOPENTCPOPTIMIZER(..., Name, Value) forwards all
% remaining name-value pairs to the OPENTCPOPTIMIZER constructor. See
% "help OpEnTcpOptimizer" for the supported options and methods.
-%
-% This helper keeps the public API lightweight while the implementation
-% lives in the documented OpEnTcpOptimizer class.
-
- if nargin < 2
- arg2 = [];
- end
- client = OpEnTcpOptimizer(arg1, arg2, varargin{:});
+ client = OpEnTcpOptimizer(varargin{:});
end
From 3aaa4bfa7870d995c9f4b35fdb89a8d03af0c19a Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Thu, 26 Mar 2026 16:44:41 +0000
Subject: [PATCH 3/4] add changelog in matlab toolbox
---
matlab/CHANGELOG.md | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
create mode 100644 matlab/CHANGELOG.md
diff --git a/matlab/CHANGELOG.md b/matlab/CHANGELOG.md
new file mode 100644
index 00000000..c116abcd
--- /dev/null
+++ b/matlab/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Change Log
+
+All notable changes to the MATLAB interface will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+Note: This is the Changelog file for the MATLAB interface of OpEn.
+
+
+## [0.1.0] - Unreleased
+
+### Added
+
+- New MATLAB TCP client in `matlab/api/OpEnTcpOptimizer.m` for TCP-enabled optimizers generated in Python.
+- Convenience constructor helper `matlab/api/createOpEnTcpOptimizer.m`.
+- Support for parametric optimizers over TCP using calls of the form `response = client.solve(p)`.
+- Support for OCP optimizers over TCP by loading `optimizer_manifest.json` and allowing named-parameter calls such as `response = client.solve('x0', x0, 'xref', xref)`.
+- Automatic packing of named OCP parameter blocks according to the manifest order, including support for manifest defaults.
+- MATLAB-side helpers for `ping`, `kill`, warm-start options, and normalized solver responses.
+
+### Changed
+
+- Added a dedicated MATLAB API area under `matlab/api` for the current interface, separate from the legacy MATLAB code.
From a6434cbb6eeed10017509c80af67d235fb3cc8c3 Mon Sep 17 00:00:00 2001
From: Pantelis Sopasakis
Date: Thu, 26 Mar 2026 16:56:39 +0000
Subject: [PATCH 4/4] matlab api: readme file
---
matlab/README.md | 205 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 205 insertions(+)
create mode 100644 matlab/README.md
diff --git a/matlab/README.md b/matlab/README.md
new file mode 100644
index 00000000..5a5ae63d
--- /dev/null
+++ b/matlab/README.md
@@ -0,0 +1,205 @@
+# OpEn MATLAB API
+
+This directory contains the MATLAB interface of **Optimization Engine (OpEn)**.
+
+The current MATLAB API lives in [`matlab/api`](./api). It communicates with
+optimizers generated in Python that expose a TCP server interface.
+
+The legacy MATLAB code is preserved in [`matlab/legacy`](./legacy).
+
+## Capabilities
+
+The current MATLAB toolbox supports:
+
+- Connecting to TCP-enabled optimizers generated in Python
+- Calling standard parametric optimizers using a flat parameter vector
+- Calling OCP-generated optimizers using named parameter blocks from
+ `optimizer_manifest.json`
+- Loading OCP manifests and, when available, automatically reading the TCP
+ endpoint from the sibling `optimizer.yml`
+- Sending `ping` and `kill` requests to the optimizer server
+- Providing optional warm-start data through:
+ - `InitialGuess`
+ - `InitialLagrangeMultipliers`
+ - `InitialPenalty`
+- Returning normalized solver responses with an `ok` flag and solver
+ diagnostics
+- Returning stage-wise `inputs` for OCP optimizers and `states` for
+ multiple-shooting OCP optimizers
+
+The main entry points are:
+
+- [`matlab/api/OpEnTcpOptimizer.m`](./api/OpEnTcpOptimizer.m)
+- [`matlab/api/createOpEnTcpOptimizer.m`](./api/createOpEnTcpOptimizer.m)
+
+## Getting Started
+
+Add the MATLAB API folder to your path:
+
+```matlab
+addpath(fullfile(pwd, 'matlab', 'api'));
+```
+
+Make sure the target optimizer TCP server is already running.
+
+## Simple Optimizers
+
+### Connect to a parametric optimizer
+
+Use a TCP port directly. The IP defaults to `127.0.0.1`.
+
+```matlab
+client = OpEnTcpOptimizer(3301);
+pong = client.ping();
+disp(pong.Pong);
+```
+
+You can also specify the endpoint explicitly:
+
+```matlab
+client = OpEnTcpOptimizer('127.0.0.1', 3301);
+```
+
+### Solve a parametric optimizer
+
+For a standard parametric optimizer, pass the flat parameter vector:
+
+```matlab
+response = client.solve([2.0, 10.0]);
+
+if response.ok
+ disp(response.solution);
+ disp(response.cost);
+else
+ error('OpEn:SolverError', '%s', response.message);
+end
+```
+
+### Solve with warm-start information
+
+```matlab
+response1 = client.solve([2.0, 10.0]);
+
+response2 = client.solve( ...
+ [2.0, 10.0], ...
+ 'InitialGuess', response1.solution, ...
+ 'InitialLagrangeMultipliers', response1.lagrange_multipliers, ...
+ 'InitialPenalty', response1.penalty);
+```
+
+### Stop the server
+
+```matlab
+client.kill();
+```
+
+## OCP Optimizers
+
+For OCP-generated optimizers, MATLAB uses **name-value pairs** to provide the
+parameter blocks listed in `optimizer_manifest.json`.
+
+### Load an OCP optimizer from its manifest
+
+If `optimizer_manifest.json` and `optimizer.yml` are in the same generated
+optimizer directory, the client can infer the TCP endpoint automatically:
+
+```matlab
+manifestPath = fullfile( ...
+ pwd, ...
+ 'open-codegen', ...
+ '.python_test_build_ocp', ...
+ 'ocp_single_tcp', ...
+ 'optimizer_manifest.json');
+
+client = OpEnTcpOptimizer('ManifestPath', manifestPath);
+disp(client.parameterNames());
+```
+
+You can also override the endpoint explicitly:
+
+```matlab
+client = OpEnTcpOptimizer(3391, 'ManifestPath', manifestPath);
+```
+
+### Solve a single-shooting OCP optimizer
+
+The following example matches the OCP manifest in
+`open-codegen/.python_test_build_ocp/ocp_single_tcp`:
+
+```matlab
+response = client.solve( ...
+ 'x0', [1.0, -1.0], ...
+ 'xref', [0.0, 0.0]);
+
+if response.ok
+ disp(response.solution);
+ disp(response.inputs);
+ disp(response.exit_status);
+else
+ error('OpEn:SolverError', '%s', response.message);
+end
+```
+
+If the manifest defines default values for some parameters, you only need to
+provide the required ones:
+
+```matlab
+manifestPath = fullfile( ...
+ pwd, ...
+ 'open-codegen', ...
+ '.python_test_build_ocp', ...
+ 'ocp_manifest_bindings', ...
+ 'optimizer_manifest.json');
+
+client = OpEnTcpOptimizer('ManifestPath', manifestPath);
+response = client.solve('x0', [1.0, 0.0]);
+```
+
+### Solve a multiple-shooting OCP optimizer
+
+For multiple-shooting OCPs, the MATLAB client also returns the state
+trajectory reconstructed from the manifest slices:
+
+```matlab
+manifestPath = fullfile( ...
+ pwd, ...
+ 'open-codegen', ...
+ '.python_test_build_ocp', ...
+ 'ocp_multiple_tcp', ...
+ 'optimizer_manifest.json');
+
+client = OpEnTcpOptimizer('ManifestPath', manifestPath);
+
+response = client.solve( ...
+ 'x0', [1.0, -1.0], ...
+ 'xref', [0.0, 0.0]);
+
+disp(response.inputs);
+disp(response.states);
+```
+
+### OCP warm-start example
+
+Warm-start options can be combined with named OCP parameters:
+
+```matlab
+response1 = client.solve( ...
+ 'x0', [1.0, -1.0], ...
+ 'xref', [0.0, 0.0]);
+
+response2 = client.solve( ...
+ 'x0', [1.0, -1.0], ...
+ 'xref', [0.0, 0.0], ...
+ 'InitialGuess', response1.solution, ...
+ 'InitialLagrangeMultipliers', response1.lagrange_multipliers, ...
+ 'InitialPenalty', response1.penalty);
+```
+
+## Notes
+
+- The MATLAB API does not start the optimizer server; it connects to a server
+ that is already running.
+- For plain parametric optimizers, use `client.solve(p)`.
+- For OCP optimizers, use `client.solve('name1', value1, 'name2', value2, ...)`.
+- The helper function `createOpEnTcpOptimizer(...)` is a thin wrapper around
+ `OpEnTcpOptimizer(...)`.