Skip to content

Commit 26e174a

Browse files
committed
feat(ovito): added ovito file import
1 parent 532210a commit 26e174a

File tree

8 files changed

+293
-13
lines changed

8 files changed

+293
-13
lines changed

src/NEXUS.Fractal/Services/ProjectService.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using NEXUS.Parsers.MDT.Models.Frames.Scanned;
2323
using NEXUS.Parsers.MDT.Models.Frames.Spectroscopy;
2424
using NEXUS.Parsers.MDT.Models.Pallete;
25+
using NEXUS.Parsers.Ovito;
2526
using ReactiveUI;
2627
using ReactiveUI.Fody.Helpers;
2728
using SixLabors.ImageSharp.PixelFormats;
@@ -47,9 +48,10 @@ public class ProjectService : StatefulServiceBase
4748
AllowMultiple = true,
4849
FileTypeFilter =
4950
[
50-
new FilePickerFileType("Все типы") {Patterns = ["*.mdt", "*.bcr", "*.jpeg", "*.jpg", "*.png", "*.bmp"]},
51+
new FilePickerFileType("Все типы") {Patterns = ["*.mdt", "*.bcr", "*.jpeg", "*.jpg", "*.png", "*.bmp", "*.xyz"]},
5152
new FilePickerFileType("NT-MDT") { Patterns = ["*.mdt"] },
5253
new FilePickerFileType("DigitalSurf") { Patterns = ["*.bcr"] },
54+
new FilePickerFileType("XYZ Ovito") { Patterns = ["*.xyz"]},
5355
new FilePickerFileType("Изображения") { Patterns = ["*.jpeg", "*.jpg", "*.png", "*.bmp"] }
5456
]
5557
};
@@ -255,6 +257,9 @@ public async Task ImportToProject()
255257
case ".bcr":
256258
ProcessBcrFile(filePath);
257259
break;
260+
case ".xyz":
261+
ProcessXyzFile(filePath);
262+
break;
258263
}
259264
}
260265
}
@@ -362,6 +367,26 @@ private void ProcessBcrFile(string filePath)
362367
Project.Frames.Add(new FrameViewModel(frameModel));
363368
}
364369

370+
private void ProcessXyzFile(string filePath)
371+
{
372+
if (Project == null)
373+
return;
374+
375+
var bcr = XYZParser.Parse(filePath).Convert();
376+
var processor = bcr.CreateFromBcrFrame();
377+
FrameModel frameModel = new FrameModel
378+
{
379+
Id = Guid.NewGuid(),
380+
SourceType = FrameSourceType.DigitalSurf,
381+
Name = Path.GetFileNameWithoutExtension(filePath),
382+
HeightMap = processor.GetHeightMap().Normalize(),
383+
HeightSpacing = 10,
384+
HeightScaling = 1,
385+
MetaData = bcr.Metadata
386+
};
387+
Project.Frames.Add(new FrameViewModel(frameModel));
388+
}
389+
365390
public async Task ExportFromProject()
366391
{
367392
var file = await _storageProvider.SaveFilePickerAsync(ExportFilePickerOptions);

src/NEXUS.Parsers.BCR/Helpers/BcrFrameImageProcessor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ private MinMax CalculateDataRange()
108108
double min = 0;
109109
double max = 0;
110110

111-
for (int i = 0; i < _frame.XPixels; i++)
111+
for (int i = 0; i < _frame.Data.GetLength(0); i++)
112112
{
113-
for (int j = 0; j < _frame.YPixels; j++)
113+
for (int j = 0; j < _frame.Data.GetLength(1); j++)
114114
{
115115
var value = _frame.Data[i, j];
116116

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
using System.Globalization;
2+
using System.Text;
3+
using NEXUS.Parsers.Ovito.Models.XYZFile;
4+
5+
namespace NEXUS.Parsers.BCR.Helpers;
6+
7+
public static class XyzToBcrConverter
8+
{
9+
public static BcrFile Convert(this XyzFile xyzFile, double pixelSize = 0.5, double atomRadius = 1.34,
10+
double baseZ = -10.0, string unit = "nm", BcrParser.BcrDataType dataType = BcrParser.BcrDataType.Float32)
11+
{
12+
if (xyzFile == null || xyzFile.Particles.Count == 0)
13+
throw new ArgumentException("XYZ файл пустой или невалидный.");
14+
15+
// Определяем bounding box
16+
double minX = xyzFile.Particles.Min(p => p.X);
17+
double maxX = xyzFile.Particles.Max(p => p.X);
18+
double minY = xyzFile.Particles.Min(p => p.Y);
19+
double maxY = xyzFile.Particles.Max(p => p.Y);
20+
21+
int xPixels = (int)Math.Ceiling((maxX - minX) / pixelSize) + 1;
22+
int yPixels = (int)Math.Ceiling((maxY - minY) / pixelSize) + 1;
23+
24+
double[,] data = new double[yPixels, xPixels];
25+
bool[,] voidMask = new bool[yPixels, xPixels];
26+
27+
// ИЗМЕНЕНИЕ: Подложка всегда имеет высоту baseZ, voidMask = false везде!
28+
for (int j = 0; j < yPixels; j++)
29+
{
30+
for (int i = 0; i < xPixels; i++)
31+
{
32+
data[j, i] = baseZ; // Подложка по умолчанию
33+
voidMask[j, i] = false; // ВАЖНО: НЕ void!
34+
}
35+
}
36+
37+
// Для каждого пикселя вычисляем высоту от атомов (перекрывает подложку)
38+
Parallel.For(0, yPixels, j => // Параллелизация для скорости
39+
{
40+
double gridY = minY + j * pixelSize;
41+
for (int i = 0; i < xPixels; i++)
42+
{
43+
double gridX = minX + i * pixelSize;
44+
double maxHeight = baseZ;
45+
46+
foreach (var atom in xyzFile.Particles)
47+
{
48+
double distXY = Math.Sqrt(Math.Pow(gridX - atom.X, 2) + Math.Pow(gridY - atom.Y, 2));
49+
if (distXY <= atomRadius) // <= вместо < для полного покрытия
50+
{
51+
double heightContribution =
52+
atom.Z + Math.Sqrt(Math.Pow(atomRadius, 2) - Math.Pow(distXY, 2));
53+
if (heightContribution > maxHeight)
54+
{
55+
maxHeight = heightContribution;
56+
}
57+
}
58+
}
59+
60+
data[j, i] = maxHeight; // Атомы перекрывают подложку
61+
}
62+
});
63+
64+
// Нормализация (сдвигаем min Z к 0)
65+
double globalMinZ = data.Cast<double>().Min();
66+
for (int j = 0; j < yPixels; j++)
67+
{
68+
for (int i = 0; i < xPixels; i++)
69+
{
70+
data[j, i] -= globalMinZ; // Подложка теперь на Z=0
71+
}
72+
}
73+
74+
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
75+
{
76+
{ "fileformat", dataType == BcrParser.BcrDataType.Int16 ? "bcrstm" : "bcrf" },
77+
{ "xpixels", xPixels.ToString() },
78+
{ "ypixels", yPixels.ToString() },
79+
{ "zunit", unit },
80+
{ "xlength", (maxX - minX + pixelSize).ToString("F3", CultureInfo.InvariantCulture) },
81+
{ "ylength", (maxY - minY + pixelSize).ToString("F3", CultureInfo.InvariantCulture) },
82+
{ "zmin", "0" }
83+
};
84+
85+
if (dataType == BcrParser.BcrDataType.Int16)
86+
{
87+
metadata["bit2nm"] = "1.0";
88+
}
89+
90+
// Добавляем комментарий о подложке
91+
metadata["comment"] = $"Generated from XYZ with substrate at Z={baseZ} and atom radius={atomRadius}";
92+
93+
return new BcrFile
94+
{
95+
XPixels = xPixels,
96+
YPixels = yPixels,
97+
Data = data,
98+
VoidMask = voidMask, // Все false - вся поверхность валидна!
99+
Metadata = metadata
100+
};
101+
}
102+
103+
/// <summary>
104+
/// Сохраняет BcrFile в файл (реализация Save для BCR).
105+
/// </summary>
106+
/// <param name="bcrFile">Объект BcrFile.</param>
107+
/// <param name="filePath">Путь к файлу.</param>
108+
public static void SaveBcrFile(BcrFile bcrFile, string filePath)
109+
{
110+
if (bcrFile == null)
111+
throw new ArgumentNullException(nameof(bcrFile));
112+
113+
bool isInt16 = bcrFile.Metadata["fileformat"].Contains("stm");
114+
double scale = ParseUnitToScale(bcrFile.Metadata.GetValueOrDefault("zunit", "nm"));
115+
double bit2nm = 1.0;
116+
if (isInt16 && bcrFile.Metadata.TryGetValue("bit2nm", out var bitStr))
117+
double.TryParse(bitStr, out bit2nm);
118+
119+
// Подготавливаем заголовок
120+
var header = new StringBuilder();
121+
foreach (var kvp in bcrFile.Metadata)
122+
{
123+
header.AppendLine($"{kvp.Key} = {kvp.Value}");
124+
}
125+
126+
// Добавляем zmin если нужно
127+
double minZ = bcrFile.Data.Cast<double>().Min();
128+
header.AppendLine($"zmin = {minZ.ToString(CultureInfo.InvariantCulture)}");
129+
130+
byte[] headerBytes = Encoding.ASCII.GetBytes(header.ToString());
131+
Array.Resize(ref headerBytes, 2048); // Фиксированный размер 2048 байт, заполняем нулями если меньше
132+
133+
// Подготавливаем данные
134+
using var ms = new MemoryStream();
135+
using var bw = new BinaryWriter(ms);
136+
137+
for (int y = 0; y < bcrFile.YPixels; y++)
138+
{
139+
for (int x = 0; x < bcrFile.XPixels; x++)
140+
{
141+
double val = bcrFile.Data[y, x] / scale;
142+
if (bcrFile.VoidMask[y, x])
143+
{
144+
if (isInt16) bw.Write((short)32767);
145+
else bw.Write(1.7e38f);
146+
}
147+
else
148+
{
149+
if (isInt16)
150+
{
151+
short scaledVal = (short)(val / bit2nm);
152+
bw.Write(scaledVal);
153+
}
154+
else
155+
{
156+
bw.Write((float)val);
157+
}
158+
}
159+
}
160+
}
161+
162+
// Сохраняем файл
163+
using var fs = new FileStream(filePath, FileMode.Create);
164+
fs.Write(headerBytes, 0, headerBytes.Length);
165+
fs.Write(ms.ToArray(), 0, (int)ms.Length);
166+
}
167+
168+
private static double ParseUnitToScale(string unit)
169+
{
170+
unit = unit.Trim().ToLowerInvariant();
171+
return unit switch
172+
{
173+
"nm" => 1.0,
174+
"um" => 1e3,
175+
"mm" => 1e6,
176+
"m" => 1e9,
177+
_ => 1.0
178+
};
179+
}
180+
}

src/NEXUS.Parsers.BCR/NEXUS.Parsers.BCR.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<ItemGroup>
1414
<ProjectReference Include="..\NEXUS.Parsers.MDT\NEXUS.Parsers.MDT.csproj" />
15+
<ProjectReference Include="..\NEXUS.Parsers.Ovito\NEXUS.Parsers.Ovito.csproj" />
1516
</ItemGroup>
1617

1718
</Project>

src/NEXUS.Parsers.Ovito/Models/XYZFile/Particle.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.Globalization;
22

3-
namespace NEXUS.Parsers.Ovito.Models.CoordinateFile;
3+
namespace NEXUS.Parsers.Ovito.Models.XYZFile;
44

55
public class Particle(string element, double x, double y, double z)
66
{

src/NEXUS.Parsers.Ovito/Models/XYZFile/XYZFile.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
namespace NEXUS.Parsers.Ovito.Models.CoordinateFile;
1+
namespace NEXUS.Parsers.Ovito.Models.XYZFile;
22

3-
public class XYZFile
3+
public class XyzFile
44
{
55
public int AtomCount { get; set; }
66
public string Comment { get; set; }
77
public List<Particle> Particles { get; set; }
88

9-
public XYZFile()
9+
public XyzFile()
1010
{
1111
Particles = [];
1212
}

src/NEXUS.Parsers.Ovito/XYZParser.cs

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
using System.Globalization;
2-
using NEXUS.Parsers.Ovito.Models.CoordinateFile;
2+
using NEXUS.Parsers.Ovito.Models.XYZFile;
33

44
namespace NEXUS.Parsers.Ovito;
55

6-
public class XYZParser
6+
public static class XYZParser
77
{
8-
public XYZFile Parse(string filePath)
8+
public static XyzFile Parse(string filePath)
99
{
1010
if (!File.Exists(filePath))
1111
throw new FileNotFoundException($"File not found: {filePath}");
@@ -14,7 +14,7 @@ public XYZFile Parse(string filePath)
1414
if (lines.Length < 2)
1515
throw new FormatException("Invalid XYZ file format: too few lines");
1616

17-
var xyzFile = new XYZFile();
17+
var xyzFile = new XyzFile();
1818

1919
// Parse atom count (first line)
2020
if (!int.TryParse(lines[0].Trim(), out int atomCount))
@@ -54,10 +54,83 @@ public XYZFile Parse(string filePath)
5454
if (xyzFile.Particles.Count != xyzFile.AtomCount)
5555
throw new FormatException($"Atom count mismatch: expected {xyzFile.AtomCount}, found {xyzFile.Particles.Count}");
5656

57-
return xyzFile;
57+
return RemoveFloatingAtoms(xyzFile);
5858
}
5959

60-
public void Save(string filePath, XYZFile xyzFile)
60+
public static XyzFile RemoveFloatingAtoms(this XyzFile xyzFile, double connectionThreshold = 2.0, double surfaceZThreshold = 10.0)
61+
{
62+
if (xyzFile?.Particles == null || xyzFile.Particles.Count == 0)
63+
return xyzFile;
64+
65+
var particles = xyzFile.Particles;
66+
var connectedAtoms = new HashSet<int>();
67+
var surfaceAtoms = new List<int>();
68+
69+
// Находим атомы поверхности (нижние по Z координате)
70+
for (int i = 0; i < particles.Count; i++)
71+
{
72+
if (particles[i].Z <= surfaceZThreshold)
73+
{
74+
surfaceAtoms.Add(i);
75+
connectedAtoms.Add(i);
76+
}
77+
}
78+
79+
// Рекурсивно находим все связанные атомы от поверхности
80+
var atomsToCheck = new Queue<int>(surfaceAtoms);
81+
82+
while (atomsToCheck.Count > 0)
83+
{
84+
int currentIndex = atomsToCheck.Dequeue();
85+
var currentParticle = particles[currentIndex];
86+
87+
// Проверяем всех соседей
88+
for (int i = 0; i < particles.Count; i++)
89+
{
90+
if (connectedAtoms.Contains(i))
91+
continue;
92+
93+
var neighborParticle = particles[i];
94+
double distance = CalculateDistance(currentParticle, neighborParticle);
95+
96+
// Если атом находится в пределах порогового расстояния, считаем его связанным
97+
if (distance <= connectionThreshold)
98+
{
99+
connectedAtoms.Add(i);
100+
atomsToCheck.Enqueue(i);
101+
}
102+
}
103+
}
104+
105+
// Создаем новый файл только с связанными атомами
106+
var filteredFile = new XyzFile
107+
{
108+
Comment = $"{xyzFile.Comment} (floating atoms removed)",
109+
AtomCount = connectedAtoms.Count
110+
};
111+
112+
foreach (int index in connectedAtoms.OrderBy(i => i))
113+
{
114+
filteredFile.Particles.Add(particles[index]);
115+
}
116+
117+
return filteredFile;
118+
}
119+
120+
private static double CalculateDistance(Particle a, Particle b)
121+
{
122+
double dx = a.X - b.X;
123+
double dy = a.Y - b.Y;
124+
double dz = a.Z - b.Z;
125+
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
126+
}
127+
128+
public static void Save(this XyzFile xyzFile, string filePath)
129+
{
130+
Save(filePath, xyzFile);
131+
}
132+
133+
public static void Save(string filePath, XyzFile xyzFile)
61134
{
62135
if (xyzFile == null)
63136
throw new ArgumentNullException(nameof(xyzFile));

0 commit comments

Comments
 (0)