Skip to content

Commit fde0187

Browse files
sebgodclaude
andcommitted
v4.0.0: Rectangular arrays for image data (~31% faster), fix read-only NullRef
Image data now uses rectangular multi-dimensional arrays (float[,], float[,,]) instead of jagged arrays, eliminating ~12,500 heap allocations and enabling zero-copy SIMD reads on .NET 10+ via MemoryMarshal.GetArrayDataReference. 299MB test image: 285ms -> 197ms. Also fixes NullReferenceException when closing/flushing read-only files (BufferedDataStream._out null guard) and incorrect FileMode.OpenOrCreate for read-only BufferedFile access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64c68cb commit fde0187

10 files changed

Lines changed: 330 additions & 60 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
## Ignore Visual Studio temporary files, build results, and
22
## files generated by popular Visual Studio add-ons.
33

4+
# Large FITS test files (download separately)
5+
tests/CSharpFITS.Test/testdocs/LDN1089_singleFrame.fits
6+
*.local.json
7+
*.lock.json
8+
49
*.cobertura.xml
510

611
# User-specific files

CSharpFITS/CSharpFITS.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
<LangVersion>9.0</LangVersion>
1212
<PackageReadmeFile>README.md</PackageReadmeFile>
1313
<PackageLicenseFile>license.txt</PackageLicenseFile>
14-
<AssemblyVersion>3.2.0.0</AssemblyVersion>
15-
<FileVersion>3.2.0.0</FileVersion>
14+
<AssemblyVersion>4.0.0.0</AssemblyVersion>
15+
<FileVersion>4.0.0.0</FileVersion>
1616
<PackageTags>Astronomy,FITS,Image</PackageTags>
1717
<RepositoryUrl>https://github.com/SharpAstro/FITS.Lib</RepositoryUrl>
1818
<PackageProjectUrl>https://github.com/SharpAstro/FITS.Lib</PackageProjectUrl>
1919
<Description>Supports reading and writing FITS files</Description>
20-
<Version>3.2.0</Version>
20+
<Version>4.0.0</Version>
2121
<Authors>Virtual Observatory - India</Authors>
2222
<PackageId>FITS.Lib</PackageId>
2323
<RootNamespace>nom.tam</RootNamespace>

CSharpFITS/fits/ImageData.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ public override void Read(ArrayDataIO i)
338338
}
339339
else
340340
{
341-
dataArray = ArrayFuncs.NewInstance(dataDescription.type, dataDescription.dims);
341+
dataArray = ArrayFuncs.NewRectangularInstance(dataDescription.type, dataDescription.dims);
342342
try
343343
{
344344
i.ReadArray(dataArray);
@@ -395,7 +395,7 @@ public override void Write(ArrayDataIO o)
395395
else if (dataArray == null && dataDescription != null)
396396
{
397397
// Need to create an array to match a specified header.
398-
dataArray = ArrayFuncs.NewInstance(dataDescription.type, dataDescription.dims);
398+
dataArray = ArrayFuncs.NewRectangularInstance(dataDescription.type, dataDescription.dims);
399399
}
400400
else
401401
{

CSharpFITS/image/ImageTiler.cs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public virtual Object CompleteImage
3737
throw new IOException("Attempt to read from null file");
3838
}
3939
long currentOffset = f.Position;
40-
Object o = ArrayFuncs.NewInstance(base_Renamed, dims);
40+
Object o = ArrayFuncs.NewRectangularInstance(base_Renamed, dims);
4141
f.Seek(fileOffset, SeekOrigin.Begin);
4242
f.ReadArray(o);
4343
f.Seek(currentOffset, SeekOrigin.Begin);
@@ -232,19 +232,16 @@ protected internal virtual void FillTile(Array data, Array o, int[] dims, int[]
232232
protected internal virtual void FillMemData(Array data, int[] posits, int length,
233233
Array output, int outputOffset, int dim)
234234
{
235-
// FIX THIS n-D crap
236-
//if(data is Object[])
237235
if (ArrayFuncs.CountDimensions(data) > 1)
238236
{
239237
if (ArrayFuncs.IsArrayOfArrays(data))
240238
{
241-
//Object[] xo = (Object[]) data;
242-
//FillMemData((Array)xo[posits[dim]], posits, length, output, outputOffset, dim + 1);
243239
FillMemData((Array)data.GetValue(posits[dim]), posits, length, output, outputOffset, dim + 1);
244240
}
245241
else
246242
{
247-
throw new Exception("Called FillMemData with multi-dimensional array.");
243+
// Rectangular multi-dimensional array: compute linear offset and use Buffer.BlockCopy
244+
FillMemDataRectangular(data, posits, length, output, outputOffset);
248245
}
249246
}
250247
else
@@ -269,6 +266,43 @@ protected internal virtual void FillMemData(Array data, int[] posits, int length
269266
}
270267
}
271268

269+
/// <summary>Fill tile data from a rectangular multi-dimensional array.</summary>
270+
private void FillMemDataRectangular(Array data, int[] posits, int length,
271+
Array output, int outputOffset)
272+
{
273+
int lastDim = dims.Length - 1;
274+
int startFrom = posits[lastDim];
275+
int startTo = outputOffset;
276+
int copyLength = length;
277+
278+
if (posits[lastDim] < 0)
279+
{
280+
startFrom -= posits[lastDim];
281+
startTo -= posits[lastDim];
282+
copyLength += posits[lastDim];
283+
}
284+
if (posits[lastDim] + length > dims[lastDim])
285+
{
286+
copyLength -= (posits[lastDim] + length - dims[lastDim]);
287+
}
288+
289+
if (copyLength <= 0) return;
290+
291+
// Compute linear offset into the rectangular array
292+
int linearOffset = 0;
293+
int stride = 1;
294+
for (int d = lastDim; d >= 0; d--)
295+
{
296+
linearOffset += posits[d] < 0 ? 0 : posits[d] * stride;
297+
stride *= dims[d];
298+
}
299+
// Adjust for the startFrom correction
300+
linearOffset += (startFrom - (posits[lastDim] < 0 ? 0 : posits[lastDim]));
301+
302+
int elementSize = System.Runtime.InteropServices.Marshal.SizeOf(base_Renamed);
303+
Buffer.BlockCopy(data, linearOffset * elementSize, output, startTo * elementSize, copyLength * elementSize);
304+
}
305+
272306
/// <summary>File a tile segment from a file.</summary>
273307
/// <param name="output">The output tile.</param>
274308
/// <param name="delta">The offset from the beginning of the image in bytes.</param>

CSharpFITS/util/ArrayFuncs.cs

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -142,27 +142,31 @@ public static int ComputeSize(Object o)
142142
if (t.IsArray)
143143
{
144144
int size = 0;
145+
var array = (Array)o;
145146

146-
if (t.HasElementType && t.GetElementType().IsPrimitive)
147+
// Rectangular multi-dimensional array: total elements * element size
148+
if (array.Rank > 1 && t.HasElementType && t.GetElementType().IsPrimitive)
149+
{
150+
size = array.Length * (int)sizes[t.GetElementType()];
151+
}
152+
else if (t.HasElementType && t.GetElementType().IsPrimitive)
147153
{
148-
var array = (Array)o;
149154
if (array.Length > 0)
150155
{
151-
IEnumerator i = ((Array)o).GetEnumerator();
156+
IEnumerator i = array.GetEnumerator();
152157
i.MoveNext();
153158

154159
size += array.Length * ComputeSize(i.Current);
155160
}
156161
}
157162
else
158163
{
159-
for (IEnumerator i = ((Array)o).GetEnumerator(); i.MoveNext();)
164+
for (IEnumerator i = array.GetEnumerator(); i.MoveNext();)
160165
{
161166
size += ComputeSize(i.Current);
162167
}
163168
}
164169

165-
//return size;
166170
result = size;
167171
}
168172
else if (t == typeof(String))
@@ -444,27 +448,23 @@ public static Type GetBaseClass(Object o)
444448

445449
if (o.GetType().IsArray)
446450
{
447-
// if 'o' is a jagged array of 2 or more than 2 dimensions
448-
// with all dimensions of type premitive,
449-
// check for the type of last dimension.
450-
if (o.GetType().ToString().StartsWith("System.Array"))
451+
// Rectangular multi-dimensional array (e.g., float[,,])
452+
if (((Array)o).Rank > 1)
451453
{
452-
return GetBaseClass(((Array)o).GetValue(0));
454+
return o.GetType().GetElementType();
453455
}
454-
// if 'o' is a multidimension array of any dimension
455-
// or a jagged array of 2 or more than 2 dimension having
456-
// first dimension of non-primitive type,
457-
// return the type of 'o' directly without dimension brackets.
458-
else
456+
// Jagged array of arrays (e.g., Array[] of Array[] of float[])
457+
if (o.GetType().ToString().StartsWith("System.Array"))
459458
{
460-
return Type.GetType(o.ToString().Substring(0, o.ToString().IndexOf("[")));
459+
return GetBaseClass(((Array)o).GetValue(0));
461460
}
461+
// 1D or jagged typed array - strip all bracket notation to get base type
462+
return Type.GetType(o.ToString().Substring(0, o.ToString().IndexOf("[")));
462463
}
463464
else
464465
{
465466
return o.GetType();
466467
}
467-
468468
}
469469

470470
/// <summary>This routine returns the base class of an object with the dimension indication
@@ -582,19 +582,27 @@ public static Object Flatten(Object input)
582582
protected internal static int DoFlatten(Object input, Array output, int offset)
583583
{
584584
int result = 0;
585+
var array = (Array)input;
586+
587+
// Rectangular multi-dimensional array: use Buffer.BlockCopy for efficiency
588+
if (array.Rank > 1 && array.GetType().GetElementType().IsPrimitive)
589+
{
590+
int elementSize = (int)sizes[array.GetType().GetElementType()];
591+
Buffer.BlockCopy(array, 0, output, offset * elementSize, array.Length * elementSize);
592+
return array.Length;
593+
}
585594

586595
if (IsArrayOfArrays(input))
587596
{
588-
int i = offset;
589-
for (IEnumerator e = ((Array)input).GetEnumerator(); e.MoveNext();)
597+
for (IEnumerator e = array.GetEnumerator(); e.MoveNext();)
590598
{
591599
Object a = e.Current;
592600
result += DoFlatten(a, output, offset + result);
593601
}
594602
}
595603
else if (input.GetType().IsArray)
596604
{
597-
IEnumerator e = ((Array)input).GetEnumerator();
605+
IEnumerator e = array.GetEnumerator();
598606
for (int i = offset; e.MoveNext(); ++i)
599607
{
600608
output.SetValue(e.Current, i);
@@ -643,19 +651,13 @@ public static Array Curl(Array input, int[] dimens)
643651

644652
for (IEnumerator i = input.GetEnumerator(); i.MoveNext() && NextIndex(index, dimens);)
645653
{
646-
//NewInstance call creates a jagged array. So we cannot set the value using Array.SetValue
647-
//as it works for multi-dimensional arrays and not for jagged
648-
//newArray.SetValue(i.Current, index);
649-
650654
Array tarr = newArray;
651655
for (int i2 = 0; i2 < index.Length - 1; i2++)
652656
{
653657
tarr = (Array)tarr.GetValue(index[i2]);
654658
}
655659
tarr.SetValue(i.Current, index[index.Length - 1]);
656660
}
657-
//int offset = 0;
658-
//DoCurl(input, newArray, dimens, offset);
659661
return newArray;
660662
}
661663
/// <summary>
@@ -791,22 +793,26 @@ public static Array NewInstance(Type cl, int[] dims, int dim)
791793

792794
return a;
793795
}
794-
/*
795-
Array o = Array.CreateInstance(cl, dims);
796-
if(o == null)
796+
}
797+
798+
/// <summary>Create a rectangular (multi-dimensional) array with a single allocation.
799+
/// For 1D arrays, returns a simple typed array. For 2D+, returns e.g. float[,] or float[,,].
800+
/// This is more efficient than jagged arrays for image data.</summary>
801+
/// <param name="cl">The element type.</param>
802+
/// <param name="dims">The dimensions.</param>
803+
public static Array NewRectangularInstance(Type cl, int[] dims)
804+
{
805+
if (dims.Length == 0)
797806
{
798-
System.String desc = cl + "[";
799-
System.String comma = "";
800-
for(int i = 0; i < dims.Length; i += 1)
801-
{
802-
desc += comma + dims[i];
803-
comma = ",";
804-
}
805-
desc += "]";
806-
throw new System.OutOfMemoryException("Unable to allocate array: " + desc);
807+
dims = new int[] { 1 };
807808
}
808-
return o;
809-
*/
809+
810+
if (dims.Length == 1)
811+
{
812+
return NewInstance(cl, dims[0]);
813+
}
814+
815+
return Array.CreateInstance(cl, dims);
810816
}
811817

812818

@@ -869,6 +875,27 @@ public static bool ArrayEquals(Object x, Object y, double tolf, double told)
869875

870876
if (xClass != yClass)
871877
{
878+
// Allow comparing jagged vs rectangular arrays with the same base type and dimensions
879+
if (x is Array xa && y is Array ya)
880+
{
881+
Type xBase = GetBaseClass(x);
882+
Type yBase = GetBaseClass(y);
883+
int[] xDims = GetDimensions(x);
884+
int[] yDims = GetDimensions(y);
885+
if (xBase == yBase && xDims.Length == yDims.Length)
886+
{
887+
bool sameDims = true;
888+
for (int i = 0; i < xDims.Length; i++)
889+
if (xDims[i] != yDims[i]) { sameDims = false; break; }
890+
if (sameDims && xBase.IsPrimitive)
891+
{
892+
// Flatten both and compare bytes
893+
Object fx = Flatten(x);
894+
Object fy = Flatten(y);
895+
return ArrayEquals(fx, fy, tolf, told);
896+
}
897+
}
898+
}
872899
return false;
873900
}
874901

0 commit comments

Comments
 (0)