Initial Commit

This commit is contained in:
2026-09-15 15:24:03 -04:00
commit 4dc7086e7e
605 changed files with 101380 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
function tf = contains (str, pat, varargin)
%CONTAINS True if a pattern occurs in a string (MATLAB R2016b+).
%
% One of the owuitools compatibility shims. This Octave has no
% `contains`, so a perfectly ordinary MATLAB line failed here and the
% model spent calls rediscovering `strfind`. The rule for everything in
% this directory: implement the documented behaviour for the forms that
% are actually written, and raise a clear error for the rest. A shim
% that quietly does something *close to* MATLAB is worse than one that
% is not there, because the run then certifies the wrong answer.
%
% Text here means a char row vector or a cell array of them - this
% Octave has no string type, and pretending otherwise is exactly the
% quiet difference this file exists to avoid.
if (nargin < 2)
error ('contains: not enough input arguments');
end
ignore_case = false;
if (numel (varargin) == 2 && ischar (varargin{1}) ...
&& strcmpi (varargin{1}, 'IgnoreCase'))
ignore_case = logical (varargin{2});
elseif (~isempty (varargin))
error ('contains: the only supported option is ''IgnoreCase''');
end
strs = owuitools_text_cell (str, 'contains', 'str');
pats = owuitools_text_cell (pat, 'contains', 'pat');
if (isempty (pats))
error ('contains: pat must not be empty');
end
tf = false (size (strs));
for i = 1:numel (strs)
s = strs{i};
for j = 1:numel (pats)
p = pats{j};
if (ignore_case)
hit = ~isempty (strfind (lower (s), lower (p)));
else
hit = ~isempty (strfind (s, p));
end
if (hit)
tf(i) = true;
break;
end
end
end
if (ischar (str))
tf = tf(1);
end
end
+38
View File
@@ -0,0 +1,38 @@
function bins = discretize (x, edges, varargin)
%DISCRETIZE Which bin each value falls in, for explicit edges (R2015a+).
%
% An owuitools compatibility shim; see contains.m for the policy, and
% histcounts.m for why explicit edges only and for the edge convention,
% which this shares: the last bin is closed, everything outside the
% edges is NaN, and so is NaN itself.
if (nargin < 2)
error ('discretize: not enough input arguments');
end
if (~isnumeric (edges) || isscalar (edges))
error (['discretize: this sandbox supports discretize(x, edges) with explicit ' ...
'edges only - MATLAB''s automatic binning is not reproduced here.']);
end
if (~isempty (varargin))
error ('discretize: this shim takes only (x, edges) - no categorical or option arguments');
end
if (~isnumeric (x) && ~islogical (x))
error ('discretize: x must be numeric');
end
edges = edges(:).';
if (numel (edges) < 2 || any (diff (edges) <= 0))
error ('discretize: edges must be a vector of at least two increasing values');
end
v = double (x);
bins = nan (size (v));
last = numel (edges) - 1;
for i = 1:last
if (i < last)
in = v >= edges(i) & v < edges(i + 1);
else
in = v >= edges(i) & v <= edges(i + 1);
end
bins(in) = i;
end
end
+47
View File
@@ -0,0 +1,47 @@
function [n, edges] = histcounts (x, edges, varargin)
%HISTCOUNTS Bin counts for explicit edges (MATLAB R2014b+).
%
% An owuitools compatibility shim; see contains.m for the policy.
%
% Explicit edges only, and that limit is the whole point: MATLAB's
% automatic binning picks the number of bins by a documented but
% non-trivial rule, and a shim that guessed a different one would hand
% back a different histogram without anyone noticing. Called without
% edges, this says so instead.
%
% Edge convention, which is where histcounts and the older `histc`
% differ and why this is not a one-line alias: bin i is
% [edges(i), edges(i+1)) and the LAST bin is closed, so a value exactly
% on the final edge lands in the last bin rather than in one of its own.
if (nargin < 1)
error ('histcounts: not enough input arguments');
end
if (nargin < 2 || ~isnumeric (edges) || isscalar (edges))
error (['histcounts: this sandbox supports histcounts(x, edges) with explicit ' ...
'edges only. MATLAB''s automatic bin selection is not reproduced here - ' ...
'choose the edges, e.g. histcounts(x, linspace(min(x), max(x), 11)), and ' ...
'keep whatever form you want in the code you hand over.']);
end
if (~isempty (varargin))
error ('histcounts: this shim takes only (x, edges) - no name/value options');
end
if (~isnumeric (x) && ~islogical (x))
error ('histcounts: x must be numeric');
end
edges = edges(:).';
if (numel (edges) < 2 || any (diff (edges) <= 0))
error ('histcounts: edges must be a vector of at least two increasing values');
end
v = double (x(:));
v = v(~isnan (v));
n = zeros (1, numel (edges) - 1);
for i = 1:numel (n)
if (i < numel (n))
n(i) = sum (v >= edges(i) & v < edges(i + 1));
else
n(i) = sum (v >= edges(i) & v <= edges(i + 1));
end
end
end
+41
View File
@@ -0,0 +1,41 @@
function tf = matches (str, pat, varargin)
%MATCHES True where text equals a pattern exactly (MATLAB R2019b+).
%
% An owuitools compatibility shim; see contains.m for the policy.
% Unlike `contains` this is whole-string equality, which is the point
% of it - the two are easy to confuse and MATLAB keeps them apart.
if (nargin < 2)
error ('matches: not enough input arguments');
end
ignore_case = false;
if (numel (varargin) == 2 && ischar (varargin{1}) ...
&& strcmpi (varargin{1}, 'IgnoreCase'))
ignore_case = logical (varargin{2});
elseif (~isempty (varargin))
error ('matches: the only supported option is ''IgnoreCase''');
end
strs = owuitools_text_cell (str, 'matches', 'str');
pats = owuitools_text_cell (pat, 'matches', 'pat');
tf = false (size (strs));
for i = 1:numel (strs)
for j = 1:numel (pats)
if (ignore_case)
hit = strcmpi (strs{i}, pats{j});
else
hit = strcmp (strs{i}, pats{j});
end
if (hit)
tf(i) = true;
break;
end
end
end
if (ischar (str))
tf = tf(1);
end
end
+13
View File
@@ -0,0 +1,13 @@
function [v, idx] = maxk (x, k, varargin)
%MAXK The k largest elements, in descending order (MATLAB R2017b+).
%
% An owuitools compatibility shim; see contains.m for the policy.
% Column-wise for a matrix, like MATLAB and like `sort`. NaNs go last,
% which is also what MATLAB does. `k` larger than the data returns
% everything there is rather than erroring, again matching MATLAB.
if (nargin < 2)
error ('maxk: not enough input arguments');
end
[v, idx] = owuitools_topk (x, k, 'descend', 'maxk', varargin{:});
end
+10
View File
@@ -0,0 +1,10 @@
function [v, idx] = mink (x, k, varargin)
%MINK The k smallest elements, in ascending order (MATLAB R2017b+).
%
% An owuitools compatibility shim; see contains.m for the policy.
if (nargin < 2)
error ('mink: not enough input arguments');
end
[v, idx] = owuitools_topk (x, k, 'ascend', 'mink', varargin{:});
end
+25
View File
@@ -0,0 +1,25 @@
function c = owuitools_text_cell (value, fname, argname)
%OWUITOOLS_TEXT_CELL Normalise char / cellstr input for the shims.
%
% Shared by the text shims in this directory. Returns a cell array of
% char rows, and refuses anything else by name so the message reads
% like the function the caller actually wrote.
%
% The name is prefixed rather than generic because this directory sits
% ahead of everything on the path: a file called `text_cell.m` here
% would shadow anyone's own helper of that name, in their own script,
% with no way for them to see why.
if (ischar (value))
if (ndims (value) > 2 || (~isempty (value) && rows (value) > 1))
error ('%s: %s must be a single row of characters, not a char matrix', fname, argname);
end
c = {value};
elseif (iscellstr (value))
c = value;
elseif (iscell (value))
error ('%s: %s must be text - a char row or a cell array of char rows', fname, argname);
else
error ('%s: %s must be text, not %s', fname, argname, class (value));
end
end
+30
View File
@@ -0,0 +1,30 @@
function [v, idx] = owuitools_topk (x, k, direction, fname, varargin)
%OWUITOOLS_TOPK Shared body of the maxk/mink shims.
if (~isempty (varargin))
error ('%s: this shim takes only (x, k) - no dimension or option arguments', fname);
end
if (~isnumeric (x) && ~islogical (x))
error ('%s: x must be numeric', fname);
end
if (~isnumeric (k) || ~isscalar (k) || k < 0 || k ~= fix (k))
error ('%s: k must be a non-negative integer', fname);
end
if (isvector (x) || isempty (x))
[sorted, order] = sort (x(:), direction);
n = min (k, numel (sorted));
v = sorted(1:n);
idx = order(1:n);
if (size (x, 1) == 1 && ~isempty (x)) % a row in, a row out
v = v.';
idx = idx.';
end
return;
end
[sorted, order] = sort (x, 1, direction);
n = min (k, size (sorted, 1));
v = sorted(1:n, :);
idx = order(1:n, :);
end
+278
View File
@@ -0,0 +1,278 @@
function varargout = print (varargin)
%PRINT Transparent shim: render PNGs through gnuplot's cairo terminal.
%
% The model writes ordinary MATLAB - print(f,'plot.png','-dpng') - and must
% keep writing ordinary MATLAB, because whatever it writes is what the user
% runs. But gnuplot's plain `png` terminal does not anti-alias, and draws
% every line as a 1-pixel stroke regardless of LineWidth. On a steep curve
% that needs several vertical pixels per column it leaves gaps, so a 4 Hz
% sine came out as a field of disconnected dashes rather than a wave.
%
% `pngcairo` anti-aliases and renders the same LineWidth as a continuous
% stroke. Swapping it in here means the picture is right without the code
% being wrong: -dpngcairo is Octave-only and would fail in MATLAB, so it
% must never appear in anything the model writes.
%
% The layout goes the same way. Canvas size, type size and where the key
% sits are properties of this renderer, not of the analysis, so they are
% set here rather than in lines the model would have to remember to write.
% Three fixes, each explained at its helper below:
%
% __owuitools_canvas__ a big canvas WITHOUT print's font scaling
% __owuitools_type__ label type the size gnuplot reserves for
% __owuitools_legends_below__ the key under the axes, not over the data
%
% Every one of them is undone before this function returns: the figure is
% handed back exactly as the script left it, because the script may draw
% into it again and print it a second time.
%
% This file lives outside the working directory and is put on the path
% ahead of Octave's own print, so the user's files are untouched.
args = varargin;
has_device = false;
has_size = false;
has_resolution = false;
wants_png = false;
% The layout work below is about pixels, so it covers every picture the
% tool can hand back - .png, .jpg, .gif - while the cairo swap is about
% one gnuplot terminal and stays with PNG alone.
raster_ext = {'.png', '.jpg', '.jpeg', '.gif'};
raster_dev = {'-dpng', '-djpg', '-djpeg', '-dgif'};
wants_raster = false;
for k = 1:numel (args)
if (ischar (args{k}))
a = args{k};
if (strncmpi (a, '-S', 2))
has_size = true;
elseif (strncmpi (a, '-r', 2) && numel (a) > 2)
has_resolution = true;
elseif (strncmpi (a, '-d', 2) && ! strncmpi (a, '-debug', 6))
has_device = true;
if (any (strcmpi (a, raster_dev)))
wants_raster = true;
end
if (strcmpi (a, '-dpng'))
args{k} = '-dpngcairo';
wants_png = true;
end
elseif (a(1) != '-')
[~, ~, ext] = fileparts (a);
if (any (strcmpi (ext, raster_ext)))
wants_raster = true;
wants_png = wants_png || strcmpi (ext, '.png');
end
end
end
end
% print() also infers the device from the file extension, so a bare
% print(f,'plot.png') has to be caught too.
if (wants_png && ! has_device)
args{end+1} = '-dpngcairo';
end
% Paper inches become pixels at the print resolution, which defaults to
% 150 dpi: the JPEG terminal took the 1100 pt canvas set below and made a
% 2292 px image of it, with 10 pt type marooned in the middle. pngcairo
% ignores the setting, so pinning it to 72 costs nothing there and makes
% one inch mean one hundred points wherever it is read.
if (wants_raster && ! has_size && ! has_resolution)
args{end+1} = '-r72';
end
shim_dir = fileparts (mfilename ('fullpath'));
rmpath (shim_dir);
unwind_protect
undo = {};
if (wants_raster)
fig = __owuitools_figure__ (args);
undo = [__owuitools_canvas__(fig, has_size), ...
__owuitools_type__(fig), ...
__owuitools_legends_below__(fig)];
end
unwind_protect
if (nargout > 0)
[varargout{1:nargout}] = print (args{:});
else
print (args{:});
end
unwind_protect_cleanup
for k = numel (undo):-1:1
try
set (undo{k}{1}, undo{k}{2}, undo{k}{3});
catch
end
end
end_unwind_protect
unwind_protect_cleanup
addpath (shim_dir, '-begin');
end_unwind_protect
end
function fig = __owuitools_figure__ (args)
% print(f,...) names its figure; print('plot.png',...) means the current one.
fig = [];
try
for k = 1:numel (args)
if (! ischar (args{k}) && isscalar (args{k}) && ishghandle (args{k}))
fig = ancestor (args{k}, 'figure');
return;
end
end
if (! isempty (get (0, 'currentfigure')))
fig = gcf ();
end
catch
fig = [];
end
end
function undo = __owuitools_canvas__ (fig, has_size)
%__OWUITOOLS_CANVAS__ A bigger picture without doubling the type with it.
%
% -S is the obvious way to ask for a 1100 px canvas and the wrong one.
% print.m reads it as "a six-inch figure blown up to 1100 px" and scales
% every font in the figure by
%
% width / 6 / screenpixelsperinch (72 here, so width/432)
%
% which at the old -S900,675 was 2.08x: the 10 pt axes font was drawn at
% 16.7 pt and the 11 pt title at 18.3 pt on a canvas only 900 px wide.
% That is what pushed titles off the right edge, sat axis labels on top
% of tick labels, and ran bar-chart category names into each other. A
% bigger -S never helped, because the type grew with the canvas.
%
% Sizing by paper instead takes the other branch of print.m, where the
% canvas is paperposition x 72 and the scale factor stays 1 - so the type
% keeps the size Octave gave it and a bigger canvas buys real room.
%
% The shape follows the figure: a script that made a wide figure gets a
% wide PNG. Only the width is fixed, and only when the caller did not ask
% for a size itself.
undo = {};
if (has_size || isempty (fig))
return;
end
try
width = 1100;
pos = get (fig, 'position');
aspect = 0.75;
if (numel (pos) == 4 && pos(3) > 0 && pos(4) > 0)
aspect = min (1.5, max (0.4, pos(4) / pos(3)));
end
undo = {{fig, 'paperunits', get(fig, 'paperunits')}, ...
{fig, 'paperposition', get(fig, 'paperposition')}, ...
{fig, 'paperpositionmode', get(fig, 'paperpositionmode')}};
set (fig, 'paperunits', 'inches');
set (fig, 'paperposition', [0, 0, width/72, round(width*aspect)/72]);
set (fig, 'paperpositionmode', 'manual');
catch
end
end
function undo = __owuitools_type__ (fig)
%__OWUITOOLS_TYPE__ Axis labels the size gnuplot reserved room for.
%
% gnuplot lays a plot out in character units of the TERMINAL font, and
% Octave sets that terminal font from the axes font size - 10 pt - while
% drawing axis labels at 11, because labelfontsizemultiplier is 1.1. The
% gap is small and the result is not: the space reserved below the tick
% labels is a 10 pt line, the xlabel drawn into it is an 11 pt line, and
% it lands on the tick row. Same story on the left for the ylabel.
%
% Dropping the label multiplier to 1 makes the two agree exactly, which
% is the whole fix - verified against gnuplot directly: same plot, same
% key, xlabel at 11 pt overlaps the ticks and at 10 pt clears them.
%
% The title keeps its 1.1 and its bold weight. It sits in the top margin
% with nothing under it to collide with, and it is the one piece of text
% a reader should find first.
%
% With that tie in place the size can be chosen on its merits, and 10 pt
% - Octave's default, for a 560 px figure on a screen - is too small on a
% 1100 px canvas that a chat window then shows at 700. 12 pt is about
% where it reads at that width and still leaves six category names room
% side by side. A script that set a font size itself keeps it: that is
% what fontsizemode records.
undo = {};
if (isempty (fig))
return;
end
try
ax = findall (fig, 'type', 'axes');
for k = 1:numel (ax)
if (strcmp (get (ax(k), 'tag'), 'legend'))
continue;
end
undo{end+1} = {ax(k), 'labelfontsizemultiplier', ...
get(ax(k), 'labelfontsizemultiplier')};
set (ax(k), 'labelfontsizemultiplier', 1);
if (strcmp (get (ax(k), 'fontsizemode'), 'auto'))
undo{end+1} = {ax(k), 'fontsize', get(ax(k), 'fontsize')};
set (ax(k), 'fontsize', 12);
end
end
catch
end
end
function undo = __owuitools_legends_below__ (fig)
%__OWUITOOLS_LEGENDS_BELOW__ The key under the axes, not over the data.
%
% A legend inside the axes sits on top of what it is labelling: the key
% is opaque here (see patch-legend-opaque.sh, and transparent is worse -
% the lines then run through the text), so it hides the curves under it.
% 'best' does not help either; this backend does not implement it and
% warns, and every legend lands top-right regardless of what was asked.
%
% Below the axes nothing can collide with the data, because gnuplot
% reserves the space for an outside key and shrinks the plot to fit it.
%
% The frame comes off with the move. gnuplot packs an outside key flush
% against the x-axis label - a one-pixel gap, whatever the type size or
% the label's own offset - so the box's top edge is drawn through the
% descenders of that label and reads as an underline under it. Out on
% the white margin, with no plot lines behind the text, the frame was
% only decoration; without it the key is a caption under the plot.
% (The fill is handled one layer down: patch-legend-opaque.sh keeps an
% outside key transparent, because an opaque one erased those same
% descenders outright - "the trigger edge" printed as "the triaaer edae".)
%
% Two legends are left where they are. One a script deliberately put
% outside already - 'eastoutside' - because that is a layout decision
% rather than the default falling where it falls. And every legend in a
% figure that has more than one plot in it: a subplot grid has no margin
% under each plot to give away, only the gap before the next one's title,
% and a key dropped into that gap lands on both. Inside is where they fit
% there, and inside is where the opaque fill still applies.
undo = {};
if (isempty (fig))
return;
end
try
h = findall (fig, 'type', 'axes');
tags = get (h, 'tag');
if (! iscell (tags))
tags = {tags};
end
if (sum (! ismember (tags, {'legend', 'colorbar'})) != 1)
return;
end
for k = 1:numel (h)
if (! strcmp (get (h(k), 'tag'), 'legend'))
continue;
end
loc = get (h(k), 'location');
if (! ischar (loc) || ! isempty (strfind (lower (loc), 'outside')))
continue;
end
undo{end+1} = {h(k), 'location', loc};
set (h(k), 'location', 'southoutside');
undo{end+1} = {h(k), 'box', get(h(k), 'box')};
set (h(k), 'box', 'off');
end
catch
end
end
+35
View File
@@ -0,0 +1,35 @@
function [out, removed] = rmmissing (x, varargin)
%RMMISSING Drop missing values (MATLAB R2016b+).
%
% An owuitools compatibility shim; see contains.m for the policy.
%
% Numeric and logical data only, where "missing" means NaN. MATLAB also
% accepts tables, categoricals, strings and datetimes, where missing
% means something different in each; none of those types exist here, so
% this refuses them rather than inventing an answer. For a vector the
% missing elements go; for a matrix the whole ROW goes, as in MATLAB.
if (nargin < 1)
error ('rmmissing: not enough input arguments');
end
if (~isempty (varargin))
error ('rmmissing: this shim takes only (x) - no dimension or option arguments');
end
if (~isnumeric (x) && ~islogical (x))
error (['rmmissing: this sandbox supports numeric and logical input only, not %s - ' ...
'MATLAB''s table, string and categorical types do not exist here'], class (x));
end
if (isvector (x) || isempty (x))
removed = isnan (double (x(:)));
out = x(~removed);
if (size (x, 1) == 1 && ~isempty (x))
out = reshape (out, 1, []);
end
removed = reshape (removed, size (x));
return;
end
removed = any (isnan (double (x)), 2);
out = x(~removed, :);
end
+21
View File
@@ -0,0 +1,21 @@
function n = strlength (s)
%STRLENGTH Number of characters in each element of text (MATLAB R2016b+).
%
% An owuitools compatibility shim; see contains.m for the policy.
% MATLAB returns a double array shaped like its input, and 0 for the
% empty string. With char input it counts the characters, which is what
% `numel` does here - the difference only shows up for cell input.
if (nargin < 1)
error ('strlength: not enough input arguments');
end
if (ischar (s))
n = numel (s);
return;
end
c = owuitools_text_cell (s, 'strlength', 'str');
n = zeros (size (c));
for k = 1:numel (c)
n(k) = numel (c{k});
end
end