Skip to content

Commit bf739d9

Browse files
committed
Rebuild MATLAB project as numerical approximation study
1 parent dd7447a commit bf739d9

26 files changed

Lines changed: 617 additions & 259 deletions

‎.github/workflows/matlab.yml‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: MATLAB Tests and Reproduction
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: matlab-actions/setup-matlab@v3
15+
- uses: matlab-actions/run-tests@v3
16+
with:
17+
source-folder: src
18+
select-by-folder: tests
19+
test-results-junit: test-results/results.xml
20+
- uses: actions/upload-artifact@v4
21+
if: always()
22+
with:
23+
name: matlab-test-results
24+
path: test-results/results.xml
25+
if-no-files-found: ignore
26+
27+
reproduce:
28+
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: matlab-actions/setup-matlab@v3
33+
- uses: matlab-actions/run-command@v3
34+
with:
35+
command: "addpath('src'); addpath('experiments'); generateAllFigures;"
36+
- name: Verify expected generated artifacts
37+
shell: bash
38+
run: |
39+
test -s results/raw/convergence.csv
40+
test -s results/raw/range_reduction.csv
41+
test -s results/reference/benchmark_sweep.csv
42+
test -s results/figures/accuracy_performance.png
43+
- uses: actions/upload-artifact@v4
44+
with:
45+
name: numerical-sine-approximation-results
46+
path: results/
47+
if-no-files-found: error

‎.gitignore‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# MATLAB generated files
2+
*.asv
3+
*.m~
4+
*.autosave
5+
*.mat
6+
7+
# Machine-specific raw experiment output
8+
results/raw/*.csv
9+
10+
# Temporary OS/editor files
11+
.DS_Store
12+
.vscode/
13+
.idea/

‎README.md‎

Lines changed: 42 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,67 @@
1-
# Maclaurin Series Demo for sin(x) (MATLAB)
1+
# Numerical Approximation of the Sine Function in MATLAB
22

3-
A self-contained MATLAB demo illustrating **Maclaurin series approximations of sin(x)** using a fully numerical approach (no Symbolic Toolbox required).
3+
This repository studies how mathematically equivalent-looking approximations of `sin(x)` behave under finite-precision arithmetic. The project question is: **how accurately and efficiently can sine be approximated when algorithm design, range reduction, basis choice, and floating-point effects are all considered?** MATLAB's built-in `sin` is the practical reference implementation; this project does not claim to replace it.
44

5-
The script visualizes approximation quality, error behavior, convergence via an animated GIF, a 3D error surface, and includes a classic **non-analytic counterexample** where the Maclaurin series fails.
5+
## Methods
66

7-
---
7+
| Method | Main idea |
8+
|---|---|
9+
| Direct Taylor | Explicit powers and factorials as a transparent baseline. |
10+
| Recurrence Taylor | Reuses consecutive terms to avoid repeated powers and factorials. |
11+
| Reduced Taylor | Reduces arguments to approximately `[-pi/4, pi/4]` and reconstructs by quadrant symmetry. |
12+
| Chebyshev | Computes interval-specific coefficients from Chebyshev nodes. |
13+
| Clenshaw | Evaluates Chebyshev expansions by backward recurrence. |
814

9-
## Features
15+
The numerical routines validate finite real inputs and evaluate them in MATLAB double precision; compatible numeric inputs may be converted to double by MATLAB argument validation. The simple range reducer is intended for moderate arguments, not arbitrary huge arguments or correctly rounded argument reduction.
1016

11-
- Overlay plots of `sin(x)` and multiple Maclaurin polynomials
12-
- Approximation vs absolute error for a selected polynomial degree
13-
- Animated GIF showing convergence as the degree increases
14-
- Comparison of true error with the “next-term” error proxy
15-
- 3D surface plot: error vs `x` and number of included terms
16-
- Non-analytic counterexample at `x = 0` where the Maclaurin series fails
17+
## Key mathematical ideas
1718

18-
---
19+
The study connects Taylor remainder bounds, interval approximation, cancellation, range reduction, recurrence evaluation, Clenshaw evaluation, machine spacing, unit roundoff `u = eps/2`, and accuracy–runtime trade-offs. Safeguarded relative error near a zero of sine is reported separately from ordinary relative error because its denominator is `max(abs(reference),eps)`.
1920

20-
## File
21+
## Repository structure
2122

22-
- **`maclaurin_sin_demo.m`**
23+
- `src/+numapprox`: reusable numerical algorithms.
24+
- `tests`: `matlab.unittest` tests, including invalid-input and shape tests.
25+
- `experiments`: convergence, range-reduction, Chebyshev, and floating-point studies.
26+
- `benchmarks`: degree-sweep accuracy/runtime benchmark.
27+
- `report`: [technical study](report/numerical_approximation_study.md).
28+
- `results/raw`: generated machine-specific outputs.
29+
- `results/reference`: selected outputs suitable for review after real execution.
30+
- `results/figures`: generated plots.
31+
- `.github/workflows`: MATLAB test and reproduction workflows.
2332

24-
---
33+
## Quick start and tests
2534

26-
## Requirements
27-
28-
- MATLAB (standard installation)
29-
- No Symbolic Math Toolbox required
30-
- Uses built-in MATLAB functions for plotting and GIF generation
31-
32-
---
33-
34-
## How to Run
35-
36-
1. Open MATLAB and set the **Current Folder** to the project directory.
37-
2. Run the script:
35+
From the repository root in MATLAB:
3836

3937
```matlab
40-
maclaurin_sin_demo
38+
addpath('src');
39+
results = runtests('tests', 'IncludeSubfolders', true);
40+
assert(all([results.Passed]), 'One or more tests failed.');
4141
```
4242

43-
The script will generate several figures and optionally save a GIF file in the current folder.
44-
45-
---
43+
The tests cover zero, positive and negative inputs, row/column/matrix shapes, odd symmetry, degree handling, range-reduction boundaries, invalid intervals, non-finite inputs, Chebyshev coefficient sizes, known Chebyshev polynomials, Clenshaw, error metrics, and Taylor remainder bounds.
4644

47-
## Output
45+
## Reproducing experiments
4846

49-
- Multiple MATLAB figures:
50-
- `sin(x)` vs Maclaurin approximations
51-
- Approximation and absolute error (two-panel plot)
52-
- Error vs next-term proxy
53-
- 3D error surface
54-
- Non-analytic counterexample plot
55-
- Animated GIF:
56-
- `maclaurin_sin.gif` (if enabled)
47+
`generateAllFigures` is a function, so add its directory and call it directly:
5748

58-
---
59-
60-
## Configuration
61-
62-
You can easily customize the demo by editing the parameters at the top of the script:
49+
```matlab
50+
addpath('src');
51+
addpath('experiments');
52+
generateAllFigures;
53+
```
6354

64-
- Domain and resolution of `x`
65-
- Polynomial degrees used for approximation
66-
- Degree used for error analysis
67-
- GIF generation options (enable/disable, delay time)
68-
- 3D error surface resolution
55+
This generates raw CSV files, selected reference CSV summaries, and PNG figures under `results/`. It also runs the benchmark degree sweep. The workflow can run the same entry point on demand and uploads the generated files as an artifact; it does not commit generated files automatically.
6956

70-
---
57+
## Results status
7158

72-
## Notes
59+
**RESULTS PENDING MATLAB EXECUTION in this sandbox.** No numerical benchmark, CSV, or figure is claimed here unless generated by MATLAB. After local or GitHub Actions execution, selected genuine outputs can be copied from `results/raw` to `results/reference` and committed deliberately.
7360

74-
- The “next-term proxy” provides an intuitive estimate of the error near `x = 0`, but it is **not a strict bound** for all `x`.
75-
- The non-analytic example demonstrates that even when all derivatives at a point exist and are zero, the Maclaurin series may still fail to represent the function.
61+
## Limitations
7662

77-
---
63+
The range reducer uses nearest-integer arithmetic with MATLAB's `pi` and is documented only for moderate arguments. Chebyshev coefficients are interval-specific and are not intended for extrapolation. Runtime depends on MATLAB release, hardware, JIT warm-up, and vector size. The experiment scripts are evidence-generation tools, not production replacements for MATLAB's numerical library.
7864

7965
## License
8066

81-
MIT License (recommended).
82-
Feel free to use, modify, and share for educational purposes.
67+
MIT. See [LICENSE](LICENSE).

‎benchmarks/benchmarkMethods.m‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
function tableOut = benchmarkMethods
2+
%BENCHMARKMETHODS Sweep degree, measure runtime and accuracy on a fixed grid.
3+
root = fileparts(fileparts(mfilename('fullpath'))); addpath(fullfile(root,'src'));
4+
x = linspace(-pi,pi,20001); degrees = 3:2:25; interval=[-pi,pi];
5+
methodNames = {'directTaylor';'recurrenceTaylor';'reducedTaylor';'chebyshevClenshaw'};
6+
rows = numel(methodNames)*numel(degrees); method=strings(rows,1); degree=zeros(rows,1); gridSize=zeros(rows,1); runtime=zeros(rows,1); maxError=zeros(rows,1); meanError=zeros(rows,1);
7+
release = string(version); platform = string(computer); row=0;
8+
for j=1:numel(degrees)
9+
d=degrees(j); c=numapprox.chebyshevCoefficients(@sin,interval,d);
10+
handles={@() numapprox.sinTaylorDirect(x,d), @() numapprox.sinTaylorRecurrence(x,d), @() numapprox.sinTaylorReduced(x,d), @() numapprox.sinChebyshev(x,d,interval,c)};
11+
for k=1:numel(methodNames)
12+
row=row+1; method(row)=methodNames{k}; degree(row)=d; gridSize(row)=numel(x); f=handles{k}; f();
13+
if exist('timeit','file'), runtime(row)=timeit(f); else, tic; for rep=1:10, f(); end; runtime(row)=toc/10; end
14+
y=f(); e=abs(sin(x)-y); maxError(row)=max(e); meanError(row)=mean(e);
15+
end
16+
end
17+
tableOut=table(method,degree,gridSize,runtime,maxError,meanError,repmat(release,rows,1),repmat(platform,rows,1), ...
18+
'VariableNames',{'method','degree','gridSize','runtimeSeconds','maxAbsoluteError','meanAbsoluteError','matlabRelease','platform'});
19+
writetable(tableOut,fullfile(root,'results','raw','benchmark_sweep.csv'));
20+
% A point is nondominated when no other point is no slower and no less accurate.
21+
frontier=false(height(tableOut),1);
22+
for i=1:height(tableOut)
23+
frontier(i)=~any(tableOut.runtimeSeconds <= tableOut.runtimeSeconds(i) & tableOut.maxAbsoluteError <= tableOut.maxAbsoluteError(i) & ...
24+
(tableOut.runtimeSeconds < tableOut.runtimeSeconds(i) | tableOut.maxAbsoluteError < tableOut.maxAbsoluteError(i)));
25+
end
26+
tableOut.nondominated=frontier; writetable(tableOut,fullfile(root,'results','reference','benchmark_sweep.csv'));
27+
figure('Color','w'); hold on; grid on; colors=lines(numel(methodNames));
28+
for k=1:numel(methodNames)
29+
mask=tableOut.method==methodNames{k}; loglog(tableOut.runtimeSeconds(mask),tableOut.maxAbsoluteError(mask),'o-','Color',colors(k,:),'DisplayName',methodNames{k});
30+
end
31+
mask=tableOut.nondominated; loglog(tableOut.runtimeSeconds(mask),tableOut.maxAbsoluteError(mask),'kp','MarkerFaceColor','y','DisplayName','nondominated points');
32+
xlabel('Seconds per vector evaluation'); ylabel('Maximum absolute error'); title('Accuracy–runtime sweep'); legend('Location','southwest');
33+
exportgraphics(gcf,fullfile(root,'results','figures','accuracy_performance.png'),'Resolution',180);
34+
end

‎experiments/chebyshevStudy.m‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
% CHEBYSHEVSTUDY Compare local Taylor and interval Chebyshev approximations.
2+
root = fileparts(fileparts(mfilename('fullpath'))); addpath(fullfile(root,'src'));
3+
interval = [-pi,pi]; x = linspace(interval(1),interval(2),4001);
4+
degrees = [5 9 13 17]; maxTaylor = zeros(size(degrees)); maxCheb = maxTaylor;
5+
figure('Color','w'); tiledlayout(2,2);
6+
for j = 1:numel(degrees)
7+
d = degrees(j); c = numapprox.chebyshevCoefficients(@sin,interval,d);
8+
t = numapprox.sinTaylorRecurrence(x,d); q = numapprox.sinChebyshev(x,d,interval,c);
9+
maxTaylor(j)=max(abs(sin(x)-t)); maxCheb(j)=max(abs(sin(x)-q));
10+
nexttile; semilogy(x,abs(sin(x)-t),x,abs(sin(x)-q),'LineWidth',1.1); grid on;
11+
title(sprintf('Degree %d',d)); xlabel('x'); ylabel('Absolute error'); legend('Taylor','Chebyshev');
12+
end
13+
exportgraphics(gcf,fullfile(root,'results','figures','chebyshev_error_profiles.png'),'Resolution',180);
14+
figure('Color','w'); semilogy(degrees,maxTaylor,'o-',degrees,maxCheb,'s-','LineWidth',1.2); grid on;
15+
xlabel('Degree'); ylabel('Maximum absolute error'); legend('Taylor','Chebyshev'); title('Interval error comparison');
16+
exportgraphics(gcf,fullfile(root,'results','figures','taylor_vs_chebyshev.png'),'Resolution',180);
17+
writematrix([degrees; maxTaylor; maxCheb].',fullfile(root,'results','reference','chebyshev_summary.csv'));

‎experiments/convergenceStudy.m‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
% CONVERGENCESTUDY Error versus odd Taylor degree and observed minima.
2+
root = fileparts(fileparts(mfilename('fullpath'))); addpath(fullfile(root,'src'));
3+
degrees = 1:2:61; xValues = [0.1, 1, 3, 7];
4+
errors = zeros(numel(xValues), numel(degrees));
5+
for i = 1:numel(xValues)
6+
for j = 1:numel(degrees)
7+
errors(i,j) = abs(sin(xValues(i))-numapprox.sinTaylorRecurrence(xValues(i),degrees(j)));
8+
end
9+
end
10+
[minErrors, minIndex] = min(errors,[],2); minDegrees = degrees(minIndex(:));
11+
figure('Color','w'); semilogy(degrees,errors.','o-','LineWidth',1.2); grid on;
12+
xlabel('Highest retained odd power'); ylabel('Absolute error');
13+
legend('x = 0.1','x = 1','x = 3','x = 7','Location','southwest');
14+
title('Taylor recurrence convergence and stagnation');
15+
exportgraphics(gcf,fullfile(root,'results','figures','convergence.png'),'Resolution',180);
16+
writematrix([degrees; errors],fullfile(root,'results','raw','convergence.csv'));
17+
writetable(table(xValues(:),minDegrees,minErrors,'VariableNames',{'x','degreeAtMinimum','minimumAbsoluteError'}),fullfile(root,'results','reference','convergence_minima.csv'));

‎experiments/floatingPointStudy.m‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
% FLOATINGPOINTSTUDY Compare observed error, Taylor bounds, and roundoff scale.
2+
root = fileparts(fileparts(mfilename('fullpath'))); addpath(fullfile(root,'src'));
3+
degrees = 1:2:61; xValues = [0.1, 1, 3]; errors = zeros(numel(xValues),numel(degrees)); bounds = errors; floors = errors;
4+
u = eps/2; % binary64 unit roundoff for round-to-nearest near 1
5+
for i=1:numel(xValues)
6+
for j=1:numel(degrees)
7+
a=numapprox.sinTaylorRecurrence(xValues(i),degrees(j)); errors(i,j)=abs(sin(xValues(i))-a);
8+
bounds(i,j)=numapprox.taylorRemainderBound(xValues(i),degrees(j));
9+
floors(i,j)=u*max(1,abs(sin(xValues(i))));
10+
end
11+
end
12+
figure('Color','w'); tiledlayout(1,2);
13+
nexttile; semilogy(degrees,errors.','LineWidth',1.1); hold on; semilogy(degrees,bounds.','--','LineWidth',1.0); semilogy(degrees,floors.',':','LineWidth',1.0); grid on; xlabel('Degree'); ylabel('Scale'); title('Observed error, bound, and roundoff scale'); legend('error 0.1','error 1','error 3','bound 0.1','bound 1','bound 3','u-scale 0.1','u-scale 1','u-scale 3','Location','southwest');
14+
nexttile; semilogy(degrees,errors./max(bounds,realmin),'LineWidth',1.1); grid on; xlabel('Degree'); ylabel('Observed error / bound'); title('Bound diagnostic');
15+
exportgraphics(gcf,fullfile(root,'results','figures','floating_point_study.png'),'Resolution',180);
16+
writematrix([degrees; errors; bounds; floors],fullfile(root,'results','raw','floating_point_study.csv'));

‎experiments/generateAllFigures.m‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
function generateAllFigures
2+
%GENERATEALLFIGURES Run the reproducible studies and benchmark.
3+
% Call after: addpath('src'); addpath('experiments'); generateAllFigures
4+
root = fileparts(fileparts(mfilename('fullpath')));
5+
addpath(fullfile(root,'src')); addpath(fullfile(root,'benchmarks'));
6+
for name = {'raw','reference','figures'}
7+
target = fullfile(root,'results',name{1});
8+
if ~exist(target,'dir'), mkdir(target); end
9+
end
10+
run(fullfile(root,'experiments','convergenceStudy.m'));
11+
run(fullfile(root,'experiments','rangeReductionStudy.m'));
12+
run(fullfile(root,'experiments','chebyshevStudy.m'));
13+
run(fullfile(root,'experiments','floatingPointStudy.m'));
14+
benchmarkMethods;
15+
end

‎experiments/rangeReductionStudy.m‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
% RANGEREDUCTIONSTUDY Compare Taylor methods on deterministic moderate arguments.
2+
root = fileparts(fileparts(mfilename('fullpath'))); addpath(fullfile(root,'src'));
3+
% Fixed seed-free dataset: k*pi+delta, k*pi/2+delta, and log-spaced offsets.
4+
k = [-100 -31 -7 -2 0 3 11 47 100];
5+
delta = [0.13 -0.21 0.07 0.31 -0.19 0.11 -0.23 0.17 -0.09];
6+
x1 = k*pi + delta;
7+
x2 = k*pi/2 + [-0.14 0.08 -0.22 0.16 0.05 -0.12 0.19 -0.06 0.10];
8+
x3 = [-(10.^linspace(-1,3,9)), 10.^linspace(-1,3,9)];
9+
values = unique([x1 x2 x3]);
10+
direct = zeros(size(values)); recurrence = direct; reduced = direct;
11+
for i = 1:numel(values)
12+
direct(i) = abs(sin(values(i))-numapprox.sinTaylorDirect(values(i),15));
13+
recurrence(i) = abs(sin(values(i))-numapprox.sinTaylorRecurrence(values(i),15));
14+
reduced(i) = abs(sin(values(i))-numapprox.sinTaylorReduced(values(i),15));
15+
end
16+
[~,order] = sort(abs(values)); values=values(order); direct=direct(order); recurrence=recurrence(order); reduced=reduced(order);
17+
figure('Color','w'); semilogy(abs(values),[direct;recurrence;reduced].','o-','LineWidth',1.1); grid on;
18+
xlabel('|x|'); ylabel('Absolute error'); legend('Direct Taylor','Recurrence Taylor','Reduced Taylor','Location','northwest');
19+
title('Range reduction on deterministic moderate arguments');
20+
exportgraphics(gcf,fullfile(root,'results','figures','range_reduction.png'),'Resolution',180);
21+
writematrix([values(:), direct(:), recurrence(:), reduced(:)],fullfile(root,'results','raw','range_reduction.csv'));

0 commit comments

Comments
 (0)