Main Content

Results for

Cephas
Cephas
Last activity about 3 hours ago

The day and night colours depend on how far the fox has run. Instead of recolouring everything on every frame, I turn the cycle into a small number of steps and only recolour when the step changes:
nn=.5-.5*cos(2*pi*s.dist/3200);
q=round(nn*40);
if ~isequal(s.ax.UserData,q)
s.ax.UserData=q;
% recolour the background, ground, text and obstacles here
end
The current step is stored in the axes UserData, so the check is cheap. Text works the same way. I only set a text string when the new string is different from the old one:
if ~strcmp(s.sct.String,t), s.sct.String=t; end
Setting properties that have not changed still costs time in every frame, so skipping them helps a lot in a game that redraws many times per second.
Check against your code: the first block is simplified from your renderFrame. Your real code also adds +100*(s.st==2) to q so the colours change on the game-over screen, and I left that out to keep the tip simple. The strcmp line is from your score text. Both matc
The day and night colours depend on how far the fox has run. Instead of recolouring everything on every frame, I turn the cycle into a small number of steps and only recolour when the step changes:
nn=.5-.5*cos(2*pi*s.dist/3200);
q=round(nn*40);
if ~isequal(s.ax.UserData,q)
s.ax.UserData=q;
% recolour the background, ground, text and obstacles here
end
The current step is stored in the axes UserData, so the check is cheap. Text works the same way. I only set a text string when the new string is different from the old one:
if ~strcmp(s.sct.String,t), s.sct.String=t; end
Setting properties that have not changed still costs time in every frame, so skipping them helps a lot in a game that redraws many times per second.
All the graphics objects (patches, lines and text) are created one time, when the game starts. After that, the render function only changes their data with set:
set(s.hd,'XData',s.DS{k}+DX,'YData',s.DT{k}+G+s.dy)
Creating new objects on every frame is a common reason MATLAB games feel slow. Updating objects that already exist is much cheaper. To hide an object, I set its data to NaN instead of deleting it:
set(s.ho(m),'XData',NaN,'YData',NaN)
Disclosure: the game code was written with Claude (an AI assistant). I tested the game in MATLAB and checked the details myself.
For the One-button Game Mini Hack, the code limit is 10,000 characters. I made a game called Ember Fox, where a fox runs and a stone guardian chases it, and you have one button. These are the techniques in the code that I found most useful.
Tip 1: Draw pixel-art sprites from text
Each sprite is stored as a grid of characters, where # is a filled pixel and . is empty:
A=['######';'#.##.#';'######';'.####.'];
[X,Y]=spr(A,0.6);
A helper function, spr, turns the grid into rectangles. For each row it uses diff to find where every run of # starts and ends, and each run becomes one rectangle instead of one rectangle per pixel. This keeps the sprite data small and the number of graphics objects low. All the sprites are built once, when the game starts.
If you are as excited as me to play these games but somehow something is stopping you from doing just that, you are at the right place. Follow the steps below and you will be playing in no time (maybe some time depending on your browser, network speed, etc.):
  1. Click on the game you like the look of (let's say, something that is transformative).
  2. This will open up its details page with the game code, preview video and the "Play Game" button. Click the video or the button.
  3. This will open up "MATLAB Drive" in a new browser window with a preview screenshot of a half-played game and a "Run this App" button. Click this button and nothing will seem to happen at first. And nothing will happen at any other subsequent tries. But don't worry, that's why you're here.
  4. Click on the "entry-×××××" blue hyperlink just above the preview area (underlined in red above). This will take you to a list of two files but hide the crucial blue button. Click on the "Entry×××××.mlapp" file to resurface the blue button.
  5. With expectant hands, click the "Run this App" button. This time it will reload the "MATLAB Drive" and put it in the mode to run your chosen game.
  6. Finally, you are where you expected to be in the point 3! Enjoy your game.🥳
Hi everyone,
I made Ember Fox: Outrun the Stone Guardian for the One-button Game Mini Hack. A fox runs forward, and a stone guardian rolls behind it and chases it. You must hop over obstacles to stay ahead.
Play it here: https://www.mathworks.com/matlabcentral/communitycontests/contests/10/entries/16448
(Click Play Game, then Run this App.)
The whole game fits in less than 10,000 characters. Below are the main tricks I used. I hope they help you with your own games.
How the game works
  • One button does everything: tap to hop, tap again in the air to dive.
  • If you hit an obstacle, the stone guardian gets closer. If it catches you, the game ends.
  • Diving onto a flying enemy gives bonus points.
  • Power-ups give a shield, slow motion, or push the guardian back.
  • Passing obstacles builds a combo multiplier.
  • The game gets faster the longer you play, and the sky changes from day to night.
Trick 1: Draw pixel-art sprites from text
I draw each sprite as a grid of characters. A # means a filled pixel:
A=['######';'#.##.#';'######';'.####.'];
[X,Y]=spr(A,0.6);
My helper function spr turns each row into rectangles. It uses diff to find where every run of # starts and ends. One run becomes one rectangle, not one rectangle per pixel. This keeps the number of graphics objects low and the code short.
Trick 2: Create graphics once, then only update them
In init I create every patch, line and text object one time. In renderFrame I only change them with set:
set(s.hd,'XData',s.DS{k}+DX,'YData',s.DT{k}+G+s.dy)
Creating new objects every frame makes a game slow. Updating existing ones is much faster. To hide an object, I set its data to NaN instead of deleting it.
Trick 3: Update only what changed
The day and night colours depend on how far you have run. I turn the cycle into a small number of steps and change the colours only when the step changes:
nn=.5-.5*cos(2*pi*s.dist/3200);
q=round(nn*40);
if ~isequal(s.ax.UserData,q)
s.ax.UserData=q; % change all colours here
end
I do the same for text. I only set the text when the new text is different:
if ~strcmp(h.String,t), h.String=t; end
Trick 4: One button, different actions
The game state decides what the button does:
  • On the start or game-over screen, it starts the game.
  • On the ground, it makes the fox hop.
  • In the air, it makes the fox dive down faster.
This gives the player more choices without adding more buttons.
Trick 5: Smooth difficulty and simple collisions
The speed grows quickly at first, then slows down, so the game stays fair:
s.spd=55+90*(1-exp(-s.sc/900));
Collisions are simple rectangle checks. After a hit, the fox is safe for a short time, so one mistake does not cause many hits in a row.
Trick 6: Fit the character limit
  • Use short variable names.
  • Use loops and shared code instead of repeating lines.
  • Run minihackContest.validate often to check the character count.
Known problem: the game runs smoothly in desktop MATLAB, but it can lag in MATLAB Online. For the best experience, run it on your computer with minihackContest.play.
Thank you for reading! If you have questions or ideas to make it better, please write a comment. I would love to see how you solve these problems in your own games.
I have MATLAB R2026b, the MATLAB Agentic Toolkit, and Claude Code installed.
In the first session, I asked Claude (Opus 5.5, High effort) to review the game rules, suggest MATLAB Agentic Toolkit skills, install them inside the project folder, and create a CLAUDE.md file.
In the second session, I asked Claude to create a combat game in which the player controls a simple stick figure wielding a club; clicking makes the figure swing the club. Simple monsters approach the player and drain the player’s health on contact, while hitting a monster with the club eliminates it. The game has an old-school 2D look, with a dark blue, star-filled background and light blue characters.
That's it. You can see result here: CLUB NIGHT
My best score until now is 2060.
Let me know your best score in the comments.
CLUB NIGHT - GAME OVER
I started my game for this contest, Stack OverDad, one month before the contest began. Dad tells programming jokes at the dinner table, and you have to laugh at the right moment. I was very happy with it. It had a dog, a little brother with a spoon, a window with a sun that goes down, fifty jokes, and many small animations.
Then the contest started, and I read the rules again. One line said: each entry is limited to 10,000 characters of MATLAB code.
My code had more than 70,000 characters.
So, as you can guess, the next hourse were cut, test, cut, test, cut... In this post, I will tell you what I learned on the way (from my AI friend!). Some of it surprised me.
Step 1: Cut What the Player Does Not Feel
For every part of the game, I asked: "Does the player really feel this?"
The dog: cute, but no. The brother: funny, but no. The sunset: nobody looks at the window when Dad tells a joke. Forty jokes: a game only uses ten anyway.
This was the biggest step, and the hardest, because I liked all of these things. But the game did not get worse. It got clearer. After this, my file had about 9,800 characters. I was done!
Step 2: The Surprise
The contest project has a command to check the game before you submit it:
minihackContest.validate
I ran it, and it said:
Validation failed: game.m exceeds the character limit (16145 / 10000, comments excluded).
16,145? My file had 9,800 characters. I counted again. It still had 9,800.
So I opened the validator to see how it counts. This is the line:
charCount = strlength(char(tree2str(mtree(gameFile, '-file'))));
mtree reads the code into a tree, and this removes the comments. Then tree2str writes the code again, in a clean, standard layout. The validator counts that text, not my file.
And the clean layout is not short at all. For example, this one line of mine:
if p,pr(s.T);end
comes back from tree2str like this:
if p
pr( s.T );
end
Every statement gets its own line. Every line gets indentation. Every f(a,b) becomes f( a, b ), and every [1 2 3] becomes [ 1, 2, 3 ]. My short style was not short anymore.
The good news: now I knew the real rule, so I could play by it.
Step 3: What Is Cheap and What Is Expensive
  • Expensive: many statements, deep nesting, and long lists of numbers.
  • Cheap: text inside a string. tree2str does not change a string at all.
So the plan was: fewer statements, less nesting, and move numbers into strings.
Example 1: Many Values in One Statement
s.st = 3;
s.t0 = t;
s.la = -1;
becomes one statement with deal:
[s.st, s.t0, s.la] = deal(3, t, -1);
Example 2: A Small if Without if
if n == 1
s.fl = "TEN?!";
end
With logical indexing, it is one line. If the condition is false, nothing changes:
s.fl(n == 1) = "TEN?!";
Example 3: Many Strings in One String
A list like ["PERFECT!", "GOOD!", "TOO LATE..."] pays for every ", ". One string with a separator is cheaper:
B = split("PERFECT!|GOOD!|TOO LATE...", "|");
All fifteen jokes are now one string too.
Example 4: Numbers Inside a String
This was the biggest trick. My scene (the table, the chairs, Dad, the family) is made of rectangles, ellipses, and polygons. That is hundreds of numbers.
So I stored each number as one character in a string. A character is really a number (its code), so I can turn it back:
v = double(char(z))/20 - 1025; % every character of z becomes one number again
I chose codes from a big block of normal, printable characters. They look like Chinese characters, but they are not words. I only use their codes. One small loop reads the string and draws all the shapes with patch. Now the whole scene costs about one character per number.
One warning from my own mistake: use double(char(z)), not double(z). If z is a string, double(z) gives NaN. My first test showed an empty table because of this.
I kept the readable list of shapes in a separate file, and it writes the packed strings for me.
I Had Help
I worked with an AI assistant the whole way. It read the validator with me, tested many small ideas fast, measured which part of the code cost the most, and rewrote code again and again without getting tired.
But the AI did not decide what the game is. It could tell me that the dog costs 1,700 characters. It could not tell me if the dog is worth it. Every "keep" or "cut" was my choice. I think this is the best way to work with AI: let it do the heavy lifting, and keep the decisions for yourself.
The Result
The validator now says 9,936 characters. The game still has Dad, THE LOOK, Mom's warnings, the family walking out, the ranks, and fifteen jokes.
My advice: run minihackContest.validate early. Do not count the characters yourself, because the validator does not count your file. It counts the clean version of your file.
I hope you have found this helpful. Thank you for reading.
Damdinsvren Bayasgalan
Damdinsvren Bayasgalan
Last activity on 1 Nov 2026 at 23:41

Hello everyone, I am new to Matlab.
I am trying to create an app to export the 5th column into a new sheet in the imported Excel file when I click Export button.
How can I write code in ExportButtonPushed
classdef test < matlab.apps.AppBase
% Properties that correspond to app components
properties (Access = public)
UIFigure matlab.ui.Figure
ExportButton matlab.ui.control.Button
UITable matlab.ui.control.Table
ImportButton matlab.ui.control.Button
end
% Callbacks that handle component events
methods (Access = private)
% Button pushed function: ImportButton
function ImportButtonPushed(app, event)
[file, location] = uigetfile( ...
{'*.xlsx;*.xlsm', 'Excel files'; '*.txt', 'Text files'; '*.*', 'All files'}, ...
'File Selector');
figure(app.UIFigure)
if isequal(file, 0), return, end
fullPath = fullfile(location, file);
try
[~,~,ext] = fileparts(fullPath);
if any(strcmpi(ext, {'.xlsx','.xlsm','.xls'}))
T = readtable(fullPath, 'Sheet', 1, 'VariableNamingRule', 'preserve');
else
T = readtable(fullPath, 'VariableNamingRule', 'preserve');
end
catch ME
uialert(app.UIFigure, ME.message, 'Read error');
return
end
if width(T) < 5
uialert(app.UIFigure, 'The file needs at least 5 columns.', 'Import error');
return
end
app.UITable.Data = T;
app.UITable.ColumnName = T.Properties.VariableNames;
lengthlist = T{:,5};
end
% Button pushed function: ExportButton
function ExportButtonPushed(app, event)
Recently, I encountered an issue where solve returned no solution for a system of equations, even though the system obviously has solutions. This can be easy to overlook.
For example:
syms a b c x
equs = [a*x^2+b*x+c==0;
a==1];
St = solve(equs,x)
St = Empty sym: 0-by-1
St is returned as an empty set, suggesting that there is no solution???
However, since a is constrained by the equation a == 1, the correct way is:
S = solve(equs,[x,a])
S = struct with fields:
x: [2×1 sym] a: [2×1 sym]
disp(S.x)
disp(S.a)
Here, a must also be included in the list of variables to solve for. Only then will solve treat a as an unknown, apply the constraint a == 1, and substitute it into the equation during the solving process.
symvar(equs)
Based on the output of the symvar function, a is already identified as one of the symbolic variables (unknowns) in the system of equations. However, the actual behavior of solve appears to contradict this expectation. In practice, a must still be explicitly included in the variable list passed to solve; otherwise, it is treated as a symbolic parameter rather than a variable to be solved for.
Takeaway: When using solve for a system of equations, any symbol that appears in the equations and is constrained by additional equations (such as a == 1) should generally be included in the variable list. Otherwise, solve may treat it as a symbolic parameter rather than an unknown, which can lead to an empty solution set or unexpected results.
Am I missing something here? I'd love to hear your thoughts.
I have learned that I am never, ever going to use Copilot in Simulink again, and probably never use Copilot in any Microsoft application again either. Two times I asked Copilot for some simple help on how to configure a subsystem block and BOTH TIMES it gave me incorrect information that did not work. After 10 minutes of trying to get accurate information out of Mathworks' own native AI (definitely light on the "I" part), I gave up and when to ChatGPT which immediately gave me the correct answer.
Do better Mathworks. Your bloatware is bad enough, and your rejection of systems engineering standards for your own is also pretty terrible. But your selection of an AI "assistant" is a complete failure.
Martin
Martin
Last activity on 23 Oct 2026 at 21:00

We use Timecontrol for statistic automations. Yesterday at 2026-10-220 10:00pm CET Time control stopped working. There is new new time in the "Run At" field, although the script should run every 5 minutes. We had the same problem in September, see https://de.mathworks.com/matlabcentral/answers/2184870-thingspeak-timecontrol-stopped-working-at-all?s_tid=srchtitle
Has anybody also prolems with Timecontrol?
The new installer streamlines the process of installing and setting up agentic workflow with MATLAB. If you have been intimidated by the setup process, give this a try!
https://github.com/matlab/agentic-toolkit-installer
For the past several days I have been experiencing a problem with data visualization on two of my ThingSpeak channels.
The data appears to be uploaded and stored correctly, and new entries are still being added to the channels. However, the visualizations are not working correctly.
The standard Field Charts show only a very small number of data points instead of the normal continuous measurements. Most of the points are compressed on the left side of the chart, while one or a few points appear far to the right and are connected by long straight lines. The time axis is also incorrect and sometimes shows repeated dates such as “01 Jan”.
The numeric widgets are affected as well and display incorrect values. Some custom visualizations also remain blank or fail to load.
The problem appeared suddenly and affects two separate channels which had previously been working correctly. I did not make any significant changes to the channels before the problem started.
This seems somewhat similar to the ThingSpeak chart/timeline issue reported in June 2026, when charts were displaying incorrect time ranges and the DAYS/RESULTS settings were not working correctly. However, my current problem also affects numeric widgets and custom visualizations, and the charts show incorrect dates and strangely positioned data points.
Could this be a regression or a similar issue with the ThingSpeak visualization system?
Has anyone else experienced the same problem recently?
Hey, this is a safe space to share your cool projects you know. I have pet ducks, I use ThingSpeak to open the door to their house each morning. I just finished an upgrade to the LED strip lights on my driveway that are controlled via ThingSpeak. And the valves for my drip irrigation system. You got it. Controlled by ThingSpeak. Let me know your last cool project you finished or the one you keep dreaming of starting. I need some more inspiration :)
Basically, my use case is an coding agent built with MATLAB to code MALTAB, but you can actually roll your own agnet with MATLAB AI Agent SDK for more realistic use cases. Hopefully my post gives you a starting point for a fun exploration!
The example code below shows how to write version 7.3 MAT files directly from C++ using the HDF5 library (libhdf5) and HighFive, a header-only C++ wrapper for libhdf5. Version 7.3 MAT files are HDF5-based, but contain a proprietary header in the first 512 bytes of the file.
The implementation performs three primary tasks:
First, it creates an HDF5 file with a 512-byte userblock. After data has been added into the file, the file is closed. Then a 128-byte header is written into the userblock so that the file is recognized by MATLAB as a valid version-7.3 MAT file. This is done in function “makeMatHeader”.
Second, MATLAB-specific metadata attributes are added to each dataset. Attributes such as “MATLAB_class” and “MATLAB_int_decode” inform MATLAB how each dataset should be interpreted.
Third, MATLAB-compatible complex datasets are created by overriding HighFive's default complex-number layout. HighFive uses the field names `r` and `i` by default, while MATLAB expects `real` and `imag`. A Highfive custom compound type is therefore registered for `std::complex<double>` using the MATLAB field names.
With these changes in place, C++ code can write scalar values, vectors, structs, complex arrays, and character arrays to a file that MATLAB can read as a version 7.3 MAT file.
#include <iostream>
#include <vector>
#include <complex>
#include <cstddef>
#include <fstream>
#include <string>
#include <cstdint>
#include <utility>
#include <bitset>
#include <highfive/highfive.hpp>
#include "hdf5.h"
// Modify the 512-byte userblock at the front of the HDF5 file to make it compatible with MATLAB's v7.3 MAT file format.
void makeMatHeader(std::string filename)
{
char header[512]; // MATLAB-style header for HDF5 file
memset(header, 0, sizeof(header)); // Initialize header to all zeros
// Example header content
snprintf(header, sizeof(header), "MATLAB 7.3 MAT-file, Platform: HDF5");
header[124] = 0;
header[125] = 2;
// I/M indicate little-endian format (Intel Mac/Windows)
header[126] = 'I';
header[127] = 'M';
// Write the header to the beginning of the file
std::ofstream outFile(filename, std::ios::binary | std::ios::in | std::ios::out);
outFile.seekp(0);
outFile.write(header, sizeof(header));
outFile.close();
}
// https://www.geeksforgeeks.org/dsa/inplace-m-x-n-size-matrix-transpose/
void MatrixInplaceTranspose(int *A, int rows, int cols)
{
// Moves elements in-place to achieve the transpose.
// A is a pointer to a 2D array, rows is the number of rows, and cols is the number of columns.
int size = rows*cols - 1;
int t; // holds element to be replaced, eventually becomes next element to move
int next; // location of 't' to be moved
int cycleBegin; // holds start of cycle
int i; // iterator
const int HASH_SIZE = 8192; // define a suitable hash size for the bitset. Must be at least as large as the number of elements in the matrix.
std::bitset<HASH_SIZE> b; // hash to mark moved elements. Must be large enough to cover all indices.
if (rows <= 0 || cols <= 0) {
throw std::invalid_argument("Matrix dimensions must be positive");
}
else if ((rows * cols) > HASH_SIZE)
{
throw std::invalid_argument("Matrix size exceeds hash size for in-place transpose. Increase the HASH_SIZE constant.");
}
b.reset();
b[0] = b[size] = 1;
i = 1; // Note that A[0] and A[size-1] won't move
while (i < size)
{
cycleBegin = i;
t = A[i];
do
{
// Input matrix [rows x cols]
// Output matrix [cols x rows]
// i_new = (i*rows)%(N-1)
next = (i*rows)%size;
std::swap(A[next], t);
b[i] = 1;
i = next;
}
while (i != cycleBegin);
// Get Next Move (what about querying random location?)
for (i = 1; (i < size) && b[i]; i++)
;
}
}
template <typename T>
std::vector<std::vector<T>> transpose(const std::vector<std::vector<T>>& matrix)
{
// Performs a nonconjugate transpose on a vector of vectors
// The input matrix is a vector of vectors, where each inner vector represents a row of the matrix.
// Handle empty matrix edge case
if (matrix.empty() || matrix[0].empty()) {
return {};
}
size_t rows = matrix.size();
size_t cols = matrix[0].size();
// Initialize the transposed matrix with flipped dimensions: cols x rows
std::vector<std::vector<T>> transposed(cols, std::vector<T>(rows));
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
transposed[j][i] = matrix[i][j];
}
}
return transposed;
}
// Creates a HighFive compound type for representing MATLAB-style complex numbers
// HighFive by default uses r/i but that is not compatible with MATLAB's complex number representation, which uses real/imag.
HighFive::CompoundType matlabComplexDouble () {
return {
{"real", HighFive::AtomicType<double>{}},
{"imag", HighFive::AtomicType<double>{}}
};
}
// Register the CompoundType to represent std::complex<double>
HIGHFIVE_REGISTER_TYPE(std::complex<double>, matlabComplexDouble);
int main()
{
const std::string filename = "test.mat";
// Needed for the complex number literal suffix 'i'
using namespace std::literals;
/*
* MATLAB vs C++ array layout
*
* MATLAB stores arrays in column-major order, meaning values in the same column are
* laid out next to each other in memory. Typical C++ containers such as nested std::vector and arrays
* are written in row-major order, where values in the same row are adjacent in memory.
*
* That difference matters when something such as a 2D dataset is exchanged from C++ to MATLAB. A 2x3
* matrix written from C++ in row-major order will be interpreted by MATLAB as a 3x2 matrix, transposed relative
* to the original C++ layout. The user will have to transpose the array to view the original C++ layout
* correctly.
*
* C++ developers need to be aware of the memory layout when
* exchanging multidimensional arrays with MATLAB. To maintain the structure,
* one will need to transpose the array before writing it to the mat file.
*/
// Test data
// 2x3 Array of complex double
std::vector<std::vector<std::complex<double>>> dataComplex = {{10.0 + 1.0i, 20.0 + 2.0i, 30.0 + 3.0i},
{40.0 + 4.0i, 50.0 + 5.0i, 60.0 + 6.0i}};
// 1x3 Vector of double
std::vector<double> dataDoubleVec = {1.1, 2.2, 3.3};
// 1x5 Array of integers
int dataIntArray[5] = {1, 2, 3, 4, 5};
// 2x4 Array of integers
int dataIntArray2x4[2][4] = {{1, 2, 3, 4},
{5, 6, 7, 8}};
int dataInt = 79;
double dataDouble = 3.14;
std::string dataString = "Hello, MATLAB!!!!!";
{
// Put the highfive related code into its own block so that the file gets closed when the file object is no longer in scope.
// Create a highfive file create property, get the underlying HDF5 property ID, and set a userblock size
HighFive::FileCreateProps fcp = HighFive::FileCreateProps::Empty();
hid_t fcpl_id = fcp.getId();
H5Pset_userblock(fcpl_id, 512);
HighFive::File file(filename, HighFive::File::Truncate, fcp);
// Storing a double to the file
// For something that is only a single value, must create a 1x1 dataspace
HighFive::DataSpace scalarDoubleSpace({1, 1});
// This creates a variable in the MATLAB workspace with the name "double_value"
HighFive::DataSet doubleField = file.createDataSet<double>("double_value", scalarDoubleSpace);
doubleField.write(dataDouble);
// Metadata for MATLAB compatibility
doubleField.createAttribute("MATLAB_class", std::string("double"));
// Storing an integer to the file
// For something that is only a single value, must create a 1x1 dataspace
HighFive::DataSpace scalarIntSpace({1, 1});
// This creates a variable in the MATLAB workspace with the name "int_value"
HighFive::DataSet intField = file.createDataSet<int>("int_value", scalarIntSpace);
intField.write(dataInt);
// Metadata for MATLAB compatibility
intField.createAttribute("MATLAB_class", std::string("int32"));
// Storing a C-style 1x5 array of integers to the file
// Since it is a single dimension, there is no need to move the data, just reinterpret it as a 5x1 row-major array.
// When Matlab imports it, it will perceive it as a 1x5 column-major array.
// This line casts the 1x5 array to a 5x1 array to match MATLAB's column-major order
int (*numArrayTrans5x1)[1] = reinterpret_cast<int (*)[1]>(dataIntArray);
HighFive::DataSpace intArray5x1Space({5, 1});
// This creates a variable in the MATLAB workspace with the name "int_array"
HighFive::DataSet intArrayField = file.createDataSet<int>("int_array", intArray5x1Space);
intArrayField.write(numArrayTrans5x1);
intArrayField.createAttribute("MATLAB_class", std::string("int32"));
// Storing a C-style 2x4 array of integers to the file
// For something that is a multi-dimensional array, we need to transpose the array and create a dataspace with the dimensions swapped
// so that the data is stored in column-major order.
MatrixInplaceTranspose((int*)dataIntArray2x4, 2, 4);
// After moving the values around, we need to cast the array with the new dimensions to match the new layout
// Cast the transposed 2x4 array to a 4x2 array to match MATLAB's column-major order
int (*numArrayTrans)[2] = reinterpret_cast<int (*)[2]>(dataIntArray2x4);
HighFive::DataSpace intArray2x4Space({4, 2});
// This creates a variable in the MATLAB workspace with the name "int_array_2x4"
HighFive::DataSet intArray2x4Field = file.createDataSet<int>("int_array_2x4", intArray2x4Space);
intArray2x4Field.write(numArrayTrans);
intArray2x4Field.createAttribute("MATLAB_class", std::string("int32"));
// Creating a Matlab struct (HDF5 group)
HighFive::Group my_struct = file.createGroup("my_struct");
my_struct.createAttribute("MATLAB_class", std::string("struct"));
// The only difference between storing data into a struct or as a normal variable in the MAT file is the
// the parent object you use when you do "createDataSet".
// file.createDataSet would create a normal variable, my_struct.createDataSet creates it within the "my_struct" struct.
// Storing a string to the struct so that it will be accessible as a character array in MATLAB
// For something that is a string, we create a dataspace with dimensions [string_length, 1] and save the character data accordingly
// We create a vector that has dataString.size() elements, each of which is a char vector of size 1 to store individual characters.
std::vector<std::vector<char>> text_bytes(dataString.size(), std::vector<char>(1));
// MATLAB expects character arrays to be a row vector so we reshape it accordingly since dimensions are swapped between C++ and MATLAB
for (int i = 0; i < dataString.size(); ++i) {
text_bytes[i][0] = dataString[i];
}
HighFive::DataSpace charSpace({dataString.size(), 1});
// uint16_t is required for MATLAB character arrays
HighFive::DataSet textField = my_struct.createDataSet<uint16_t>("text_value", charSpace);
textField.write(text_bytes);
// Metadata for MATLAB compatibility
textField.createAttribute("MATLAB_class", std::string("char"));
// Tell MATLAB to interpret the data as characters rather than integers
textField.createAttribute("MATLAB_int_decode", 2);
// Storing a vector to the struct
// In order to transpose the vector correctly, we first wrap it in another vector to make it a 2D array.
std::vector<std::vector<double>> transposableDoubleVec = { dataDoubleVec };
// For vectors, we let HighFive infer the dataspace from the data itself
// Transpose the vector to match MATLAB's column-major order
HighFive::DataSet doubleVectorField = my_struct.createDataSet("double_vector", transpose(transposableDoubleVec));
// Metadata for MATLAB compatibility
doubleVectorField.createAttribute("MATLAB_class", std::string("double"));
// Storing a complex (and multi-dimensional) vector to the struct
// For multi-dimensional vectors, we need to perform a noncojugate transpose on the array to match
// MATLAB's column-major order so the data layout is consistent between C++ and MATLAB.
// For vectors, we let HighFive infer the dataspace from the data itself
HighFive::DataSet complexField = my_struct.createDataSet("complex_vector", transpose(dataComplex));
// Metadata for MATLAB compatibility
complexField.createAttribute("MATLAB_class", std::string("complex"));
}
// Finalize the MATLAB-compatible HDF5 file by writing the MATLAB header into the userblock
makeMatHeader(filename);
return 0;
}
Duncan Carlsmith
Duncan Carlsmith
Last activity on 20 Sep 2026

Claudifying MATLAB
Duncan Carlsmith
Department of Physics, University of Wisconsin-Madison
Decorative image illustrating application of AI in education
Introduction
AI mobile and desktop apps are convenient interfaces for exploration. For programmatic work not subject to variations in AI response, an AI can generate standalone code and web workflows. (See e.g. Web Automation with Claude, MATLAB, Chromium, and Playwright.) Intermediate is your own code that can batch process input data with AI via API.
Large Language Models (LLMs) with MATLAB permits a script to connect to OpenAI Chat Completions and Images, Azure OpenAI, Ollama, and other services that accept the OpenAI format. This submission introduces an educational Live Script Claude API from MATLAB for Coursework that illustrates how to access Anthropic models via API with MATLAB, using example tasks relevant to physics education, and might be adapted for other AI vendor APIs or emulated for other applications. This submission is essentially the Live Script introduction.
The script lists the available Claude models, sends text, images, and PDFs to a chosen model, holds a multi-turn conversation, extracts structured data from a document, lets Claude call MATLAB functions, and grades a set of short answers against a rubric. The examples show how an instructor or a student might use an AI model as a programmable assistant, for feedback on a figure, a check of a lab report, or a first pass at grading.
The Messages API is a web service at https://api.anthropic.com/v1/messages. A program sends it a request in JSON, the text format used for structured data on the web, and receives Claude's reply in JSON. A request names a model, sets a limit on the length of the reply, and carries a list of messages. An optional system prompt holds standing instructions that apply to every turn. The service keeps no memory between requests, so each request carries the whole conversation so far. MATLAB builds each request as a struct and jsonencode converts it to JSON.
Lengths and prices are counted in tokens. A token is a fragment of text, on average about four characters of English. Each response reports how many input and output tokens it used.
Helper functions in this folder handle the web requests and the bookkeeping. claudeRequest sends one request and reports the server's error message on failure. claudeListModels returns the models available to the key. claudeConversation and askClaude hold a multi-turn conversation and add up tokens and cost. claudeImageBlock and claudePdfBlock package a file for a message, claudeText extracts the reply text, and claudeCost converts token counts to dollars.
The example inputs sit in the inputs folder, which the Python version shares, and are fictional. example_flawed_figure.png is a plot with deliberate defects. example_lab_report.pdf is a student lab report with three planted errors, and example_lab_report_page1.png is an image of its page for display. student_answers.csv holds five short answers of varying quality. Output files go to the matlab_outputs folder and carry a timestamp in the name, so no run overwrites an earlier one, and the MATLAB and Python outputs stay apart.
"Try this" blocks mark the settings to change. The references list the API documentation. The appendix on privacy and security describes where requests go, how long the vendor keeps them, and precautions for student work and for the API key. A parallel Python version of the script and helper functions is in the Python folder. These functions could be called from MATLAB or invoked from a terminal.
Acknowledgments and disclaimer
The author has no financial interest in any company or product named here. Nothing in this post is endorsed by, sponsored by, or an official position of the University of Wisconsin-Madison.
For those using Matlab and ecountering difficulty with mex and Xcode v27
Fix for Xcode 27 breaking MATLAB MEX C++ compilation:
matlab
edit(fullfile(prefdir,'mex_C++_maca64.xml'))
% Set LINKEXPORTCPP=""
% Then rebuild normally
Running Octave and other applications in a ChatGPT container
Duncan Carlsmith
Department of Physics, University of Wisconsin-Madison
Introduction
An AI assistant that can run code can write a script, run it, read the results, and fix errors before reporting back.
Prototyping MATLAB code in a Claude container with Octave describes one way I can supply Claude basic autonomous MATLAB executability by installing Octave in the Claude container instead of using an MCP connection or an HTTPS command server to hand the assistant my own Mac, with its MATLAB, the toolboxes, and the real files. This Octave hack is useful if, for example, I am on my phone while my laptop is offline or otherwise occupied.
ChatGPT runs code in a Linux container with Python, Node, and a shell. GNU Octave is not present and is not straightforwardly installed by the chat itself - an apt-get install octave fails because the container reaches selected package proxies and not the Debian repositories. This post describes a solution provided by Claude for ChatGPT, a solution provided that applies to other packages as well.
The trick is to prepare externally and upload an Octave (or whatever) package and simply drop it into the ChatGPT chat as needed with an installation instruction prompt. Once the chat accomplishes the installation, that chat is good to go. A 197 MB file uploaded to the conversation installs Octave 10.3.0 in about ten seconds. The install needs no root access, no apt, and no network connection from the container. I can then hand ChatGPT an .m file, read the numbers it prints, and retrieve the figure. The same method carries other programs built by conda-forge for Linux x86_64. The ExifTool bundle is 37 MB. The Pandoc bundle is 40 MB.
Be aware that in a ChatGPT Project environment, the uploaded package is available to all project chats but must be installed in each chat’s container that needs it, and can sometimes need a reinstall if you leave a chat in which it is installed. That installation action might be automated via persistent context -I’ve not tried that. Neither have I tried this trick with other AIs.
The rest of this submission is Claude-generated goop describing exactly how to implement this and the tests performed. It is not intended for normal sentient being consumption. Just hand it to your AI.
Background
The Claude container I used to prepare this post runs Ubuntu 24.04 with glibc 2.39, as root, with HTTPS access to an allowed set of hosts. apt-get update reaches the Ubuntu archives, pip reaches PyPI, and apt-get install octave would install Octave 8.4.0 from the distribution. That container also carries ffmpeg and ImageMagick and no Octave.
The ChatGPT container I tested runs Debian 13 with glibc 2.41, unprivileged, with a packages-only network policy. Its pip reaches an internal proxy and apt reaches nothing. Published reconnaissance reports the same policy (Willison, January 2026, container environment notes, sandbox exploration).
So the bundle is a requirement on ChatGPT and an upgrade on Claude. Ubuntu supplies Octave 8.4, the version the companion post worked around. The bundle supplies 10.3.0 on either, because it does not depend on what the distribution packages.
Those reports, and my own measurements, describe particular images in September 2026. The images change. The bundle therefore targets an application binary interface rather than a named vendor image. Conda-forge packages for linux-64 declare a minimum glibc version and an instruction-set baseline, and a Linux host new enough to meet both runs the same binaries. After the design was fixed, ldd --version in the ChatGPT container reported Debian GLIBC 2.41, above the glibc 2.28 floor the bundle assumes.
The method
Micromamba, a single statically linked executable, installs an explicit list of conda packages from local archive files. The bundle contains those files, a Linux micromamba binary and a short installer.
  1. On your own machine, solve the dependency graph for Linux x86_64 without installing it.
  2. Download the package archives returned by the solve.
  3. Put the archives, micromamba and the installer in one tar file.
  4. Upload the tar, extract it in the container and install under /mnt/data.
Step 4 needs no network, no root access and no system package manager. Everything is written below one directory the container already permits.
Building the Octave bundle
DROP='(qt-|qt6|pyqt-|qscintilla2|libllvm|libclang|ghostscript|mesa)' \
sh build_bundle.sh octave octave
The complete solve contains 181 packages, 460 MB of archives, and installs to 1.8 GB, much of it supporting a graphical interface the container cannot display. Dropping Qt, PyQt, QScintilla, LLVM, Clang, Mesa and Ghostscript leaves 171 packages, a 197 MB tar file and a 773 MB installed tree. Appendix A passes that exclusion list through DROP.
There is a limit to the pruning. octave-cli links libOpenGL.so.0, so libglvnd must remain even though no window is drawn.
Solving on macOS for a Linux target
My first solve on an Apple silicon Mac returned Octave 7.1.0, Python 3.9, Qt 5.12 and a 323 MB tar. The same solve run on Linux returned Octave 10.3.0, Python 3.14 and a 197 MB tar.
The difference comes from conda virtual packages, one of which is __glibc. A Mac has no glibc, so builds requiring __glibc >=2.17 appear unsatisfiable, and the solver walks backward to builds from 2022 and earlier, before that declaration was used. Setting CONDA_OVERRIDE_GLIBC=2.12 on a Linux machine reproduced the Mac result package for package.
Three settings remove the host from the decision.
export CONDA_OVERRIDE_GLIBC=2.28 # describe the target, not this Mac
export CONDA_OVERRIDE_ARCHSPEC=x86_64 # portable baseline, not the host CPU
# on the micromamba command line:
--no-rc --no-env --override-channels # ignore local conda configuration
CONDA_OVERRIDE_ARCHSPEC matters on a fast host. A solve on a machine with AVX-512 selected a metapackage pinned to x86_64_v4, which would fail in a container whose processor lacks those instructions.
Appendix A also rejects a solved Octave version below 10, which catches the silent fallback before hundreds of megabytes are downloaded.
An earlier bug in the build script was less obvious. Its URL filter matched only the linux-64 subdirectory and silently dropped 15 noarch packages. Octave still ran, and figure text fell back to whatever fonts the container had, because the DejaVu fonts are noarch.
Verification
Appendix C contains 13 checks. Each compares an Octave result with a value obtained independently. A truncated package set or a broken BLAS can start normally and return a plausible wrong answer.
The checks cover dense and sparse linear algebra, an ODE, quadrature, an FFT, root finding, minimization, MAT-file and text I/O, basic data structures and a PNG figure. The eigenvalues of the 3 by 3 second-difference matrix are compared with 2 - 2cos(k pi/4), and the ode45 value at t = 5 with the closed-form solution of y' = -2y + sin(t).
All 13 pass in the ChatGPT container. The 400 by 400 dense solve gives a residual of 1.3e-15 in 17 ms. The BLAS is real.
Octave test in ChatGPT
Figure 1. Output of the self-test under Octave 10.3.0 in a ChatGPT container. Blue solid curve, sin(x). Red dashed curve, sin(x)exp(-x/6). Black circles, the ode45 solution of y' = -2y + sin(t), sampled every 25th step.
What is missing
Octave itself does not provide MATLAB's table or string class. Octave Forge packages are unavailable because pkg install tries to download them.
The Qt graphics toolkit needs a display, so the self-test uses gnuplot and print -dpngcairo to write a PNG. Pruning Ghostscript removes EPS and PDF output and the plain -dpng device, and putting it back costs about 60 MB.
Three warnings accompany every figure. GraphicsMagick reports its 8-bit limit, Ghostscript is absent, and fontconfig prints an initialization notice. None changed the PNG output in these tests.
What persisted in the tests
I measured three behaviors in one ChatGPT project in September 2026.
Within one conversation, the container persisted across turns. An Octave install made at the start remained usable for the rest of that chat.
After several hours away, the installed directory was gone and the files uploaded or produced in the conversation had returned, with fresh timestamps. Reinstallation took one instruction and about ten seconds, with no new upload.
$ ls -la /mnt/data/
-rw-r--r-- 1 root oai_shared 206766592 Sep 13 14:40 octave-bundle-linux64.tar
-rw-r--r-- 1 root oai_shared 34124 Sep 13 14:52 octave_selftest.png
A new conversation in the same project started with an empty /mnt/data. Project files were available to ChatGPT for retrieval and did not appear in that conversation's container filesystem, so files belonged to the conversation rather than to the project.
I therefore keep one conversation as the Octave conversation and return to it whenever an .m file needs a trial run.
Other programs
ExifTool resolves to six packages and a 37 MB upload including micromamba. Pandoc resolves to one 23 MB package and a 40 MB bundle. Both use the same two scripts.
sh build_bundle.sh exiftool exiftool
sh build_bundle.sh pandoc pandoc
The Pandoc bundle converted the Markdown source of this post to RTF.
Try pip first. The Python package proxy worked here, and a bundle is unnecessary when pip supplies the program. The bundle earns its keep for compiled programs with shared-library dependencies, where apt would ordinarily be the answer.
The method does not solve a large-data problem. At the time of these tests, an upload was limited to 512 MB, so a program needing gigabytes of auxiliary data, such as the index files of a blind astrometric solver, does not fit merely because its executable does.
Security
The installer runs package scripts supplied by conda-forge, the same trust decision as creating an ordinary conda environment. The container is disposable and had no general outbound network access in these tests, which limits what a malicious package could reach. It does not make an untrusted package safe.
Wrap up
One upload and about ten seconds of installation gave a ChatGPT conversation a working Octave, and the same pair of scripts carried ExifTool and Pandoc. Octave is still not MATLAB. Parser differences, unsupported classes and toolbox calls remain, so the finished program needs a run in MATLAB.
For anyone already running a command server on a personal machine, a custom GPT action can call an HTTPS endpoint that runs MATLAB itself, on hardware that remembers yesterday's installation. The uploaded bundle is for the case in between, with no MATLAB connection, no root access and no useful apt, but a serviceable Linux container once the right files arrive.
Acknowledgments and disclaimer
The methods and measurements were assembled and tested by the author. A first draft of this post was prepared with Claude and revised with ChatGPT and further revised by the author.
The author has no financial interest in any company or product named here. Nothing in this post is endorsed by, sponsored by, or an official position of the University of Wisconsin-Madison. The affiliation is given for identification only.
Appendix A. build_bundle.sh
#!/bin/sh
# Build a self-contained, network-free bundle of any conda-forge package for
# a Linux x86_64 container. Runs on macOS or Linux. Nothing is installed on
# the host. Requires curl, tar and python3.
#
# Usage: sh build_bundle.sh <name> <package> [package ...]
# e.g. sh build_bundle.sh docs pandoc graphviz
# Output: <name>-bundle-linux64.tar in the current directory.
#
# Env: DROP extended regex of package names to exclude, for example
# DROP='(qt-|qt6|pyqt-|qscintilla2|libllvm|libclang|ghostscript|mesa)'
# GLIBC glibc version the target container provides. Default 2.28.
set -e
[ $# -ge 2 ] || { echo "usage: sh build_bundle.sh <name> <package> [package ...]"; exit 1; }
SCRIPTDIR=$(cd "$(dirname "$0")" && pwd)
NAME=$1; shift
PKGS=$*
WORK="$PWD/${NAME}-bundle-build"
mkdir -p "$WORK"
DROP=${DROP:-'(^$)'}
GLIBC=${GLIBC:-2.28}
# Describe the TARGET container, not this host. Without these a macOS solve
# falls back to pre-2022 builds, and a fast host selects AVX-512 packages.
export CONDA_OVERRIDE_GLIBC=$GLIBC
export CONDA_OVERRIDE_ARCHSPEC=x86_64
ISO='--no-rc --no-env --override-channels' # ignore ~/.condarc and CONDA_* vars
cd "$WORK"
case "$(uname -s)/$(uname -m)" in
Darwin/arm64) HOSTPLAT=osx-arm64 ;;
Darwin/x86_64) HOSTPLAT=osx-64 ;;
Linux/aarch64) HOSTPLAT=linux-aarch64 ;;
*) HOSTPLAT=linux-64 ;;
esac
echo "Host $HOSTPLAT, target linux-64 glibc $GLIBC, packages: $PKGS"
mkdir -p hostbin bundle/pkgs
curl -sSL "https://micro.mamba.pm/api/micromamba/$HOSTPLAT/latest" \
| tar -xj -C hostbin --strip-components=1 bin/micromamba
chmod +x hostbin/micromamba
curl -sSL https://micro.mamba.pm/api/micromamba/linux-64/latest \
| tar -xjO bin/micromamba > bundle/micromamba
chmod +x bundle/micromamba
echo "Solving ..."
export MAMBA_ROOT_PREFIX="$WORK/solveroot"
./hostbin/micromamba create -n solve $ISO --platform linux-64 -c conda-forge \
$PKGS -y --dry-run --json > solve.json
python3 - "$DROP" <<'PY' > urls.txt
import json, re, sys
drop = re.compile('/' + sys.argv[1])
links = json.load(open('solve.json'))['actions']['LINK']
# A host with no glibc override can silently select an old Octave.
octave = next((p for p in links if p['name'] == 'octave'), None)
if octave and int(octave['version'].split('.')[0]) < 10:
raise SystemExit('refusing solved Octave version ' + octave['version'])
out = open('bundle/explicit.txt', 'w')
out.write("# offline install list, linux-64\n# platform: linux-64\n@EXPLICIT\n")
kept = 0
for p in links:
url = p.get('url') or "%s/%s/%s-%s-%s.conda" % (
p['channel'], p['platform'], p['name'], p['version'], p['build_string'])
if drop.search(url):
continue
md5 = p.get('md5')
out.write('@PKGS@/' + url.rsplit('/', 1)[1] + ('#' + md5 if md5 else '') + '\n')
print(url)
kept += 1
out.close()
print("kept %d of %d packages" % (kept, len(links)), file=sys.stderr)
PY
echo "Downloading $(wc -l < urls.txt) packages ..."
xargs -n1 -P8 curl -sS -L -O --output-dir bundle/pkgs < urls.txt
cp "$SCRIPTDIR/install_bundle.sh" bundle/
COPYFILE_DISABLE=1 tar -cf "../${NAME}-bundle-linux64.tar" -C bundle .
cd ..
du -h "${NAME}-bundle-linux64.tar"
cat <<EOF
Bundle: $PWD/${NAME}-bundle-linux64.tar
Upload it, then extract and run install_bundle.sh in the container.
EOF
Appendix B. install_bundle.sh
#!/bin/sh
# Install a bundle built by build_bundle.sh into a prefix inside a container.
# No network. No root. No apt.
# Usage: sh install_bundle.sh [prefix] default: <bundle dir>/prefix
set -e
HERE=$(cd "$(dirname "$0")" && pwd)
PREFIX=${1:-$HERE/prefix}
chmod +x "$HERE/micromamba"
sed "s|@PKGS@|file://$HERE/pkgs|" "$HERE/explicit.txt" > "$HERE/explicit_resolved.txt"
MAMBA_ROOT_PREFIX="$HERE/mroot" "$HERE/micromamba" create -p "$PREFIX" --offline -y \
--file "$HERE/explicit_resolved.txt" >/dev/null
# One launcher that sets the environment and runs any binary from the prefix.
cat > "$HERE/run" <<EOF
#!/bin/sh
export PATH="$PREFIX/bin:\$PATH"
export LD_LIBRARY_PATH="$PREFIX/lib:\${LD_LIBRARY_PATH:-}"
[ -d "$PREFIX/share/octave" ] && export OCTAVE_HOME="$PREFIX"
[ -f "$PREFIX/etc/fonts/fonts.conf" ] && export FONTCONFIG_FILE="$PREFIX/etc/fonts/fonts.conf"
[ -d "$PREFIX/share/gnuplot" ] && export GNUPLOT_DRIVER_DIR="$PREFIX/libexec/gnuplot"
exec "\$@"
EOF
chmod +x "$HERE/run"
echo "Installed to $PREFIX"
echo "Executables:"
ls "$PREFIX/bin" | head -20
echo "Run with: $HERE/run <command> [args]"
Appendix C. octave_selftest.m
% octave_selftest.m
% Self-check for the Octave bundle installed in a ChatGPT container.
% Each test compares against a value known independently of Octave.
% Run: /mnt/data/oct/run octave-cli -q octave_selftest.m
% Writes octave_selftest.png next to the working directory.
npass = 0;
function r = chk(name, got, want, tol)
ok = all(abs(got(:) - want(:)) <= tol);
printf('%-28s %s got %-14.8g want %-14.8g\n', name, ...
merge(ok, 'PASS', 'FAIL'), got(1), want(1));
r = ok;
end
printf('Octave %s on %s\n\n', version(), computer());
% 1. Dense linear algebra. magic(4) is singular, rank 3.
A = magic(4);
npass += chk('rank(magic(4))', rank(A), 3, 0);
% 2. Eigenvalues of the 3x3 second-difference matrix, 2 - 2cos(k pi/4).
T = full(gallery('tridiag', 3, -1, 2, -1));
want = sort(2 - 2*cos((1:3)*pi/4));
npass += chk('eig(tridiag) vs closed form', sort(eig(T))', want, 1e-12);
% 3. Solve a 400x400 system, check the residual and time it.
n = 400; rand('seed', 42);
M = rand(n) + n*eye(n); b = ones(n,1);
t0 = tic; x = M\b; dt = toc(t0);
npass += chk('400x400 solve residual', norm(M*x - b, inf), 0, 1e-10);
printf('%-28s %.3f s\n', 'solve wall time', dt);
% 4. ode45 against y(t) = (2 sin t - cos t)/5 + (6/5)exp(-2t),
% the solution of y' = -2y + sin(t), y(0) = 1.
f = @(t,y) -2*y + sin(t);
opt = odeset('RelTol', 1e-9, 'AbsTol', 1e-11);
[T5, Y5] = ode45(f, [0 5], 1, opt);
ya = @(t) (2*sin(t) - cos(t))/5 + 1.2*exp(-2*t);
npass += chk('ode45 vs analytic at t=5', Y5(end), ya(5), 1e-8);
% 5. Quadrature: integral of exp(-x^2) on [0,Inf) is sqrt(pi)/2.
npass += chk('quadgk vs sqrt(pi)/2', quadgk(@(x) exp(-x.^2), 0, Inf), sqrt(pi)/2, 1e-10);
% 6. FFT of a pure tone: peak bin amplitude n/2 for unit sine.
n = 1024; k = 17; s = sin(2*pi*k*(0:n-1)/n);
S = abs(fft(s));
npass += chk('fft peak bin', S(k+1), n/2, 1e-8);
% 7. Root finding and unconstrained minimization.
npass += chk('fzero cos(x)=x', fzero(@(x) cos(x)-x, [0 1]), 0.73908513321516, 1e-10);
npass += chk('fminsearch Rosenbrock', ...
fminsearch(@(v) 100*(v(2)-v(1)^2)^2 + (1-v(1))^2, [-1.2 1], ...
optimset('TolX',1e-8,'TolFun',1e-10)), [1 1], 1e-3);
% 8. Sparse matrices.
Sp = sparse(1:5, 1:5, 2:6);
npass += chk('sparse trace', full(sum(diag(Sp))), 20, 0);
% 9. MAT file round trip in v7 format, which MATLAB also reads.
Mg = magic(5); save('-v7', 'selftest.mat', 'Mg'); clear Mg
load('selftest.mat');
npass += chk('save/load -v7', sum(Mg(:)), 325, 0);
% 10. Text file I/O.
fid = fopen('selftest.txt', 'w'); fprintf(fid, '%d\n', 1:100); fclose(fid);
v = dlmread('selftest.txt');
npass += chk('fprintf/dlmread', sum(v), 5050, 0);
% 11. Strings, cells, structs, regexp.
c = cellfun(@(s) upper(s), {'ab','cd'}, 'UniformOutput', false);
st = struct('a', {1,2,3});
tok = regexp('Octave 10.3.0', '(\d+)\.(\d+)\.(\d+)', 'tokens');
ok = isequal(c, {'AB','CD'}) && numel(st) == 3 && str2double(tok{1}{1}) == 10;
printf('%-28s %s\n', 'cell/struct/regexp', merge(ok, 'PASS', 'FAIL'));
npass += ok;
% 12. Figure to PNG through the gnuplot toolkit.
warning('off', 'Octave:gnuplot-graphics');
graphics_toolkit('gnuplot');
fh = figure('visible', 'off');
set(fh, 'position', [0 0 900 400]);
x = linspace(0, 4*pi, 400);
plot(x, sin(x), 'b-', 'linewidth', 1.2); hold on
plot(x, sin(x).*exp(-x/6), 'r--', 'linewidth', 1.5);
kk = 1:25:numel(T5);
plot(T5(kk), Y5(kk), 'ko', 'markersize', 5, 'markerfacecolor', 'none');
legend('sin(x)', 'sin(x) exp(-x/6)', 'ode45 solution y(t)', ...
'location', 'northeastoutside');
xlabel('x'); ylabel('amplitude');
title('sin(x), damped sin(x), and ode45 solution of y prime = -2y + sin(t)');
ylim([-1.3 1.3]); grid on
print(fh, 'octave_selftest.png', '-dpngcairo', '-r120');
d = dir('octave_selftest.png');
ok = ~isempty(d) && d.bytes > 5000;
printf('%-28s %s %d bytes\n', 'figure to PNG', merge(ok, 'PASS', 'FAIL'), ...
merge(isempty(d), 0, d.bytes));
npass += ok;
ntot = 13;
printf('\n%d of %d checks passed\n', npass, ntot);
if npass < ntot
printf('FAILURES PRESENT\n');
end
printf('Forge packages installed: %d (expected 0, pkg install needs network)\n', ...
numel(pkg('list')));
Appendix D. Commands to give ChatGPT
Upload the tar and the .m file to one conversation. A literal prompt is less likely to produce a substitute method or imagined output.
Run these shell commands in order and show me all output. Do not substitute Python for any step.
mkdir -p /mnt/data/oct
tar -xf /mnt/data/octave-bundle-linux64.tar -C /mnt/data/oct
sh /mnt/data/oct/install_bundle.sh
/mnt/data/oct/run octave-cli -q /mnt/data/yourscript.m
If a command fails, stop and show the command and its complete error output. Return any PNG files created by the script.
ExifTool and Pandoc follow the same pattern.
/mnt/data/exiftool/run exiftool -s -ImageWidth -ImageHeight image.png
/mnt/data/pandoc/run pandoc -s -f markdown -t rtf -o post.rtf post.md
If the upload is not visible at the expected path, ask for ls -la /mnt/data first. Attach the tar to the conversation in which it will run. If the extract directory is mounted without execute permission, copy the tree to a writable executable directory and install there.
If you're a student looking for tips, resources, or guidance for learning MATLAB, check out my new blog: Learn MATLAB – A Student’s Guide
The blog highlights a variety of student resources, including getting started guides, cheat sheets, learning materials, and other helpful tools to make learning MATLAB easier. I hope you find it useful!