forked from kegge13/BistroMath
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathWellhofer.pas
20165 lines (18911 loc) · 815 KB
/
Wellhofer.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
unit Wellhofer; {© Theo van Soest Delphi: 01/08/2005-29/05/2024 | FPC 3.2.2: 20/05/2022}
{$mode objfpc}{$h+}
{$I BistroMath_opt.inc}
(*
=================================================================================
This library is original work of Theo van Soest.
It is published under the GNU Lesser General Public License v3 (LGPL-3.0).
https://tldrlegal.com/license/gnu-lesser-general-public-license-v3-%28lgpl-3%29
As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent modules,and
to copy and distribute the resulting executable under terms of your choice,
provided that you also meet, for each linked independent module, the terms
and conditions of the license of that module. An independent module is a
module which is not derived from or based on this library. If you modify
this library, you may extend this exception to your version of the library,
but you are not obligated to do so. If you do not wish to do so, delete this
exception statement from your version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
for more details.
You should have received a copy of the GNU Library General Public License
along with this library; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.
=================================================================================
This library can read at least 15 different data formats and represents
a wide range of profile data types in radiotherapy.
All functions are designed to accomodate for non-equidistant ("irregular") data sets.
=================================================================================
*)
(*-------------------------------------------------------------------------------
TWellhoferData=class(TRadthData) is the main object class in this unit. It is instantiated by the GUI.
There are separate objects for most file formats; only the omnipro .txt and .rfb format as well as the sun nuclear data format
are read directly by TWellhoferData.
A further complication is that:
a) different vendors or even different versions for one vendor use different directions for the GT axis,
b) the presentation of the GT-axis to the user should be configurable to comply with the scanning software,
c) effects of rotation of the water phantom should be supported,
d) the internal data representation should be made consistent at all times.
e) irregular data sets must be supported.
TWellhoferData holds a series of identical data structures:
wSource: array[twcDataSource] of twCurveDataRec;
twcDataSource =twcFirstDataSource..twcLastDataSource;
twcFirstDataSource= dsMeasured;
twcLastDataSource = dsUnrelated;
twAnyDataSource =(dsMeasured,dsReference,dsMeasFiltered,dsRefFiltered,dsCalculated,dsBuffer,dsRefOrg,dsUnrelated,dsDefault);
As filtered versions of the raw data are used intensively, it is efficient to keep filtered versions available at all times.
The burden of this strategy is to keep the fileterd version updated at all times.
The central concept of Bistromath is to compare a measurement with a reference.
Loading a reference is complicated because the reference
a) may be already available in memory,
b) the file name may be different in older version of BistroMath,
c) can be part of a structured series from a 2D-array,
d) may be part of an unstructured multi-scan file.
Core functions:
function LoadReference(AFileName :String ='';
SetCurrentAsRefSource :Boolean =False ): Boolean;
function PrepareProfile : Boolean;
function NearestPosition(Position :twFloatType;
ASource :twcDataSource=dsMeasured;
ForceAlwaysIn :Boolean=True ): Integer;
function GetInterpolatedValue(Position :twFloatType;
ASource :twcDataSource=dsMeasured;
DefaultValue :twFloatType=0 ): twFloatType;
function GetQfittedValue(Position :twFloatType;
ASource :twcDataSource=dsMeasured;
DefaultValue :twFloatType=0 ): twFloatType;
function GetScaledQfValue(Position :twFloatType;
RelativeToCenter :Boolean;
Scaling :twScalingType;
ASource :twcDataSource=dsMeasured ): twFloatType;
function FindLevelPos(ASource :twcDataSource=dsMeasured;
ALevel :twDoseLevel=d50;
Symmetric :Boolean=True ): Boolean;
function FindEdge(ASource :twcDataSource=dsMeasured ): Boolean;
procedure FastScan(ASource :twcDataSource=dsMeasured );
function Analyse(ASource :twcDataSource=dsMeasured;
AutoCenterProfile :twAutoCenter=AC_default ): Boolean;
---------------------------------------------------------------------------------------
READING DATA - READING DATA - READING DATA - READING DATA - READING DATA - READING DATA
Most data types have their objecttype, all derived from TRadthData.
Also TWellhoferData, the main analysis engine is derived from TRadthData.
In the TWellhoferData.DualReadData function an object to everry known type is opened as far as needed.
To be as fast as possible TWellhoferData itself can find out the file type:
function TWellhoferData.EvaluateFileType(AIndentString:String=''): twcFileType;
begin
if Length(AIndentString)=0 then
AIndentString:= IdentificationStg;
if Pos(rfaID ,AIndentString)=1 then Result:= twcRFA_ascii
else if Pos(eclipseID ,AIndentString)=4 then Result:= twcEclipse
else if Pos(mccID ,AIndentString)=1 then Result:= twcMccProfile
else if Pos(wICPAIdentStg,AIndentString)=1 then Result:= twcICprofilerAscii
else if Pos(hdfID ,AIndentString)=1 then Result:= twcHdfProfile
else if Pos(xioID ,AIndentString)=1 then Result:= twcCmsProfile
else if Pos(w2ID ,AIndentString)>0 then Result:= twcW2CAD
else Result:= twcUnknown;
end;
For completeness each ascendent also must a GetFileType function.
It is very convenient that the only two known binary data type are handled by TWellhoferData itself.
Therefore the high level object can sort this out:
function TWellhoferData.IsBinary(AFileName:String=''): Boolean;
var t: twcFileType;
begin
t:= GetFileType(AFileName,True);
Result:= (t in [twcWellhoferRfb,twcWDA]);
if Result then
BinStreamType:= t;
end; {~isbinary}
In the user interface (TWellForm.DataFileOpen) this is exploited by first testing on binary types,
and when not, transferring the data to a TStringStream and go from there.
---
TRadthData.ReadData sets the scene for the reading strategy.
All in all there is a lot of object context jumping.
function TRadthData.ReadData(AStream :TStream;
AFileFormat:twcFileType=twcUnknown): Boolean;
begin
SetDefaults;
FileFormat:= AFileFormat;
//************* if stream of base type TStream ************
if not (AStream is TStringStream) then
begin
if AStream.Size>0 then
BinStream.CopyFrom(AStream,0);
//************* ascendants must reintroduce ReadBinData if applicable ************
Result:= ReadBindata;
end
else
begin
if (FLocalParser or (Parser.LineCount=0)) and (AStream is TStringStream) then
begin
AStream.Position:= 0;
//************* the stream is copied to a string stream ************
Parser.Assign(TStringStream(AStream));
end;
//************* ascendants must reintroduce ParseData if applicable ************
Result:= ParseData;
end;
end; {~readdata}
--
//************* ascendants must reintroduce ParseData if applicable and call the inherited function first ************
function TRadthData.ParseData(CheckFileTypeOnly:Boolean=False): Boolean;
begin
Result:= CheckData(nil) or CheckFileTypeOnly;
if Length(Parser.FileName)>0 then
FileName:= Parser.FileName;
end; {~parsedata}
--
//************* ascendants must reintroduce CheckData if applicable and call the inherited function first ************
function TRadthData.CheckData(AStringList:TStrings=nil): Boolean;
var i: Integer;
begin
if assigned(AStringList) then
begin
i := AStringList.Count;
IdentificationStg:= AStringList.Strings[0];
end
else with FParser do
begin
i:= LineCount;
GotoTop(True,False);
//************* for most text data types the first line is the needed IdentificationStg ************
IdentificationStg:= CurrentLine;
end;
//************* assuming each point takes a line, we can set a minimum required number of lines ************
Result:= i>=twcDefMinProfilePoints;
end; {~checkdata}
*)
interface
uses Types,Classes,Math,
TOmath,TONelderMead,TOnumparser,TObaseDef,TOconfigStrings;
{imported version D7 dd. 20/12/2018 from here}
const
twcWellhoferKey ='WellhoferObject';
twcRefDirKey ='RefDir';
twcRefDirDefaultName ='references';
twcMultiScanKey ='multiscan_';
twcSymCorrLimitKey ='SymmetryCorrection';
twcFilterKey ='Filter';
twcEnergyKey ='Energy';
twcGridKey ='Grid';
twcCalcKey ='Norm';
twcPenumbraKey ='Penumbra';
twcDrefKey ='WellhoferRefDepths';
twcDCKey ='WellhoferOD2dose';
twcDRefBeams ='WellhoferGenericBeams';
twcLinacRadiusKey ='BeamRadius';
twcWMSdetKey ='WMSdetInfo';
twcMeasAxisStandard ='ICD';
twcTankAxisStandard ='XYZ';
twcMeasAxisPermutations =twcMeasAxisStandard+',IDC,CID,CDI,DIC,DCI';
twcDefUnknown ='-';
{$IFDEF DISCRETE_FIXED_DISTANCE}
twcDefDiscretisationMult =1000000;
{$ENDIF DISCRETE_FIXED_DISTANCE}
twcDefMinDataLines = 5;
twcDefMinProfilePoints = 10;
twcDefMinScanLength_cm = 1.0;
twcDefAccNormDif = 0.05;
twcMaxLogisticRange = 0.15;
twcDefAccDivFactor = 2;
twcDefaultValue = -1;
twcDefPipsPixel_cm = 0.025;
twcDefSchusterPixel_cm = 0.5;
twcDefSchusterPoints = 88;
twcDefENRblocks = 25;
twcDefSchusterDelimiters = ';#';
twcOD2doseNumPar = 6;
twcModalityFormat = '%s%0.2f';
twcModalitySet = ['C'..'X'];
twcMultiRefMaxScan = 4;
twcMedianPointsPerThread = 50;
twcSamePositionRadius_cm = 0.02;
twcDefSigmoidRadius_cm = 1;
twcDefMinFilterWidth_cm = 0.01;
twcDefEdgeRange_cm = 2;
pddfit_I1 = 0; {This ordering is important when skipping the build-up part}
pddfit_mu1 = 1;
pddfit_mu2 = 2;
pddfit_mu3 = 3;
pddfit_mu4 = 4;
pddfit_Ib = 5;
pddfit_mub = 6;
pddfit_mubpower = 7;
pddfit_X_c = 2;
pddfitOdim = pddfit_mu4+1; { 5, low-energy photons}
pddfitPdim = pddfit_mub+1; { 7, photons, may be expanded with pddfit_mubpower}
pddfitXdim = pddfit_X_c+1; { 3, for extrapolation of photons}
pddfit_I0 = pddfit_mub+1; { 7}
pddfit_d0 = pddfit_mub+2; { 8}
pddfit_Ix = pddfit_mub+3; { 9}
pddfit_mx1 = pddfit_mub+4; {10}
pddfit_mx2 = pddfit_mub+5; {11}
pddfitEdim = pddfit_mub+6; {12, electrons}
sigmoid_LowVal = 0;
sigmoid_InflectionMajor = 1;
sigmoid_Slope = 2;
sigmoid_HighVal = 3;
SigmoidDim = sigmoid_HighVal+1;
fitCalcErrorDef = 1e10;
type
fitNameArray = array[0..pddfit_mx2] of String[7];
fitNormArray = array[0..pddfit_mx2] of Boolean;
const
pddfitPnames: array[pddfit_I1..pddfit_mubpower] of String[7]=('I1' ,'mu1','mu2','mu3','mu4','Ib','mub','b_pwr');
pddfitPnorm : array[pddfit_I1..pddfit_mubpower] of Boolean =(True ,False,False,False,False,True,False,False);
pddfitXnames: array[pddfit_I1..pddfit_X_c ] of String[7]=('I1','mu1','c');
pddfitXnorm : array[pddfit_I1..pddfit_X_c ] of Boolean =(True ,False,True);
pddfitEnames: array[pddfit_I1..pddfit_mx2 ] of String[7]=('I1' ,'mu1','mu2','mu3','mu4','Ib' ,'mub','I0','d0' ,'Ix','mx1','mx2');
pddfitEnorm : array[pddfit_I1..pddfit_mx2 ] of Boolean =(False,False,False,False,False,False,False,True,False,True,False,False);
resourcestring
twForParseError = 'Parsing error at line %d: %s%s';
twForParseRead = '%d points read%s';
twForMinPoints = 'Only %d points; at least %d required.%s';
twForBinRead1 = 'Incorrect data in profile header';
twForBinRead2 = 'File read error in profile data';
twForMinMax = 'Problem with maximum (%0.1f=%0.1f)';
twForMinScanLen = 'Length of scan %0.1f cm; at least %0.1f cm required. %s';
twForPosInMm = 'Data interpreted as mm.';
twForIllegalScan = 'Bi-directional or zero steps in scan at point %d.';
twForPointError = ' -->point %d expected';
twForUnsupported = 'Unsupported scan type: ';
twForMatch = '->Match cost function(%0.3f)=%0.5f [step %0.3f cm]';
twForFileNotFound = '[ file %s not found ]';
twForFileNotRead = '[ file %s not loaded ]';
twDiagonalFound = 'diagonal detected';
twForNMreport = 'ENR: %0.2f in %d cycles, %d restart%s, %0.1f s (amoebes: %d), max %0.2f at %0.2f cm';
twMedianFilterStg = 'median-filtered';
twQuadFilterStg = 'quad-filtered';
twGammaCalcStg = 'gamma';
twDerivativeStg = 'derivative';
twNMfitStg = 'TvSpdd-fit';
twLockedStg = ' is locked';
{ ************OVERVIEW OF FILE RECOGNITION RULES*******************************
----IBA binary rfb format (as file from disk only)----
Start with a P-type string of the version of the file.
The string itself starts with "Version:". The first byte is the length of the string.
In all investigated versions this was 14 ($0e). The next two bytes represent the integer type for the number of scangroups and, after a spacing of 6 bytes, the untyped string of 5 characters " CBEAM" should be detected for a valid header.
0E 56 65 72 73 69 6F 6E 3A 36 2E 36 2E 32 36
V e r s i o n : 6 . 6 . 2 6
01 00 FF FF 01 00 05 00 43 42 65 61 6D
int(1) $FF$FF (? ? ? ?) C B E A M
----IBA Wellhöfer v6----
Start with 'Clinic:'.
----IBA Wellhöfer v7----
Start with a date and time where both '-' and '/' are acceptable as separator in the date.
When all digits are represented with n, the pattern 'n/nnnn nn:nn:nn' or 'n/nnnn n:nn:nn' should be found to start within the first five characters in the text.
The AM/PM format is also supported here.
----Sun Nuclear disk file----
Start with 'Tab-Delimited Scan Output'.
----Sun Nuclear clipboard format----
Start with 'Delivery System'.
----WMS binary----
Start with the binary representation of the file header size (588). The first integer in the header should be 6..
4C 02 06 00
int(588) int(6)
----WMS ascii----
Start with a double quoted file name with the extension '.wda' or '.wtx': '"xxxxxxx.wxx"' or a double quoted date string '"dd-mmm-yy"'.
----RFA ascii----
Start with ':SYS'.
----MCC ascii----
Start with 'BEGIN_SCAN_DATA'.
----Elekta CMS ascii----
Start with '00001090'.
----generic ascii----
The file should contain two colums of data values without any header, interpreted as position and dose.
----Varian W2CAD ascii----
The string '$STOM' should be found somewhere in beginning of the file.
Reads a maximum of 255 bytes to validate this.
----Varian Eclipse ascii----
Start with xEFxBBxBF+'Patient Name: '
----HDF ascii----
Start with '# Track:'.
----Pips ascii----
Start with 'Type of Cross-Section:'.
----Schuster ascii----
Start with 'Profile measured on '.
----SunNuclear ICprofiler ascii (export)----
Start with 'Filename'
************ AXIS CONVENTION RULES ******************
For the TWellhoferData class the internal measurement axis in linac coordinates
is from G to T, from A to B and from Up to Down.
In most data types, read with TRadthData derived classes, represent the coordinates in
a linac coordinate system. Often the GT axis is inversed however. The mephysto data type
and OmniPro data types do also give the orientation of the XYZ tank coordinate system in
the header.
Most of the import data types represent the data as given in the original file format.
The appropriate TWellhoferData.ImportXXXProfile should set full 3D coordinates in the
TWellhoferData convention. The TWellhoferData.PrepareProfile procedure will extract 1D
positions for each data point from the 3D coordinates.}
{13/07/2015 Unrelated curve added
Unrelated may keep any data and is not cleared through any routine at read time.
It may be used to store raw reference data when not locked.}
{21/07/2015 RefOrg added.
Separate storage space for reference original (not resampled) data}
{09/06/2016 dSigmoid added}
{22/07/2016 twDefCenterType added}
{15/11/2016 twCenterUseType added}
{03/12/2016 twIgnoreParams,twIgnoreSet added}
{27/12/2017 MeasFiltered added to datasources}
{26/01/2018 dsRefFiltered added to datasources}
{28/01/2018 twcCoupledFiltered}
{06/12/2018 twWellhoferAscii_v8}
{17/06/2020 dSigmoid50 added to twDoseLevel}
{21/06/2020 added twSigmoidType}
{20/07/2020 added twcFieldClass}
{21/07/2020 added fcWedge}
{18/08/2020 added fcMRlinac}
{13/10/2020 added fcElectron}
{19/04/2021 added CenterNearOrigin}
{10/06/2021 twcArrayLimitRec: Penumbra renamed to Limit; twcArrayLimit: added CalcRangeFirst and CalcRangeLast}
{28/03/2022 added twcGammaScope}
{11/04/2022 twcLimits,twcRange, twcFloatRange}
type
twcLimits =(ptFirst,ptLast);
twcChannels =(FieldCh,RefCh);
twcFieldClass =(fcStandard,fcFFF,fcSmall,fcMRlinac,fcWedge,fcElectron); //fieldtypes, linked to twcFieldClassNames
twcEdgeClass =(fcPrimary,fcFallBack);
twcFieldShape =(Rectangular,Blocks,MLC,Circular);
twcCenterType =(CenterPenumbra,CenterOrigin,CenterNearOrigin,CenterMax); {ordering critical for user interface}
twcFFFPeakType =(CenterFFFTopModel,CenterFFFSlopes); {ordering critical for user interface}
twcDoseLevel =(dLow,dHigh,d20,d50,d80,d90,dUser,dDerivative,dInflection,dSigmoid50,dTemp); {ordering critical within code and user interface, should be checked if changed}
twcPositionUseType=(dUseBorder,dUseDerivative,dUseInflection,dUseSigmoid50,dUseOrigin,dUseMax,dUseFFFtop,dUseFFFslopes,dUseUndefined,dUseConfigured); {TAnalyseForm relies on this order, check all code!}
twcNMpddFits =(NM_Primary,NM_Extrapolation);
twcAutoCenter =(AC_default,AC_on,AC_off);
twcGammaScope =(gGammaComplete,gGammaLimited);
twcNormalisation =(NormOnCenter,NormOnOrigin,NormOnMax,NormOnInFieldArea);
twcModalityChar = 'C'..'X';
twcShiftType =(AbsShift,RelShift);
twcSourceEnum =(dsMeasured,dsReference,dsMeasFiltered,dsRefFiltered,dsCalculated,dsBuffer,dsRefOrg,dsUnrelated
{$IFDEF WELLHOFER_DUMPDATA},dsDefault{$ENDIF}); {order is used for coupling}
twcFieldSizeDesc =(fInplane,fCrossplane); //order is critical and linked twScanTypes
twcTankAxis =(X,Y,Z); //order is critical and linked twScanTypes
twcMeasAxis =(Inplane,Crossplane,Beam); //order is critical and linked twScanTypes
twcScanTypes =(snGT,snAB,snPDD,snAngle,snGenericHorizontal,snFreescan, //order of first 3 is critical and assumed in code
snGenericProfile,snFanLine,snPlane,snUndefined);
twcScalingType =(scNormalised,scAvgNorm,scPlotScaling,scMaximum);
twcBeamType =(Photons,Electrons,Protons,Other);
twcFloatType = Double; //was Extended in Delphi7
twcRange = array[twcLimits] of Integer;
twcFloatRange = array[twcLimits] of twcFloatType;
twcFloatArray = array of twcFloatType;
twcFloatArrayPtr = ^twcFloatArray;
twcAliasRec = record
twKey,twValue: String;
twApplied : Boolean;
end;
twcAliasArray = array of twcAliasRec;
twcSides = (twcLeft,twcRight);
twcArrayLimit = record
CalcPos : twcFloatType; //interpolated position with some model
CalcRangeFirst: Integer; //report which range is used
CalcRangeLast : Integer;
Nearest : Integer; //report nearest point to wanted
Valid : Boolean;
end;
twcArrayLimitRec = record
Level: twcFloatType;
Limit: array[twcSides] of twcArrayLimit;
end;
twcTankAxisChar = 'X'..'Z';
twcXYZset = set of twcTankAxisChar;
twcLimitsArray = array[twcDoseLevel] of twcArrayLimitRec;
twcOD2doseArray = array[1..twcOD2doseNumPar] of twcFloatType;
twcCoordinate = record case integer of
0: (m:array[twcMeasAxis ] of twcFloatType);
1: (t:array[twcTankAxis ] of twcFloatType);
2: (c:array[twcTankAxisChar] of twcFloatType);
3: (i:array[0..2 ] of twcFloatType);
end;
twcCoordArray = array of twcCoordinate;
twcFileType = (twcWellhoferAscii_v6, twcWellhoferAscii_v7, twcWellhoferAscii_v8, twcWellhoferRfb , twcWTX, twcWDA,
twcGenericProfile , twcHdfProfile , twcSchusterProfile , twcCmsProfile ,
twcPipsProfile , twcMccProfile , twcSNCfileAscii , twcSNCclipboard ,
twcRFA_ascii , twcW2CAD , twcEclipse , twcICprofilerAscii, twcUnknown);
twcGrafPoint = record
X,Y: twcFloatType;
end;
twcGrafProfile = array of twcGrafPoint;
wmsIntType = SmallInt; {make no changes, needed for binary files, 2 bytes!!}
wmsRealType = Single; {make no changes, needed for binary files, 4 bytes}
twcTankAxisID = packed array[twcMeasAxis ] of twcTankAxisChar;
twcTankAxisSign = record
case Boolean of
true : (c: packed array[twcTankAxisChar] of wmsIntType);
false: (t: packed array[twcTankAxis ] of wmsIntType);
end;
twcMeasAxisSign = packed array[twcMeasAxis ] of ShortInt;
twcMeasAxisStg = String[3];
twcFieldDescrArr = array[twcFieldSizeDesc] of twcFloatType;
twcGantrySetup = (twCW_180_Down,twCCW_180_Down,twCW_180_Up,twCCW_180_Up);
twcFitModels = (pddPhoton,pddPhotonExtrapolation,pddElectron,penumbraSigmoid);
twcMultiScanList = array of String;
twcIgnoreParams = (twiLinac,twiModality,twiEnergy,twiSSD,twiFieldSize,twiWedge,twiDepth,twiDiagonal,twiScanDirection,twiScanClass,twiAngle,twiMeasDivice);
twcIgnoreSet = set of twcIgnoreParams;
twcIECdefs =(twcIEC601,twcIEC1217);
twcCardinalAngles =(Rot000,Rot090,Rot180,Rot270);
twcBanks =(twcUpper,twcLower,twcReference);
const //set definitions
twcMultiFiles : set of twcFileType =[twcRFA_ascii,twcMccProfile,twcWellhoferRfb,twcW2CAD,twcICprofilerAscii];
twcHoriScans : set of twcScanTypes=[snGT,snAB,snAngle,snGenericHorizontal];
twcVertScans : set of twcScanTypes=[snPDD,snFanLine];
twcGenericFormats : set of twcFileType =[twcGenericProfile,twcHdfProfile];
twcInherentFormats: set of twcFileType =[twcWellhoferAscii_v6,twcWellhoferAscii_v7,twcWellhoferRfb,twcSNCfileAscii,twcSNCclipboard];
twcBinaryFormats : set of twcFileType =[twcWDA,twcWellhoferRfb];
twcFirstDataSource= dsMeasured;
twcLastDataSource = dsUnrelated;
twcLastRelated = dsBuffer;
twcFilterSources = [dsMeasured ,dsReference ];
twcFilteredCopies = [dsMeasFiltered,dsRefFiltered];
twcCoupledFiltered:array[dsMeasured..dsReference ] of twcSourceEnum=(dsMeasFiltered,dsRefFiltered);
twcCoupledSources :array[dsMeasFiltered..dsRefFiltered] of twcSourceEnum=(dsMeasured ,dsReference );
twcIECnames :array[twcIECdefs ] of String =('601','1217');
twcBLDnames :array[twcIECdefs,twcCardinalAngles ] of String[2] =(('X2','Y2','X1','Y1'),('Y1','X1','Y2','X2'));
twcBLDupper :array[twcIECdefs ] of Char =('Y','X');
{$IFDEF WELLHOFER_DUMPDATA}
var
DumpDataFilter:set of twcSourceEnum=[dsMeasured..dsUnrelated];
{$ENDIF}
(*------modality---------------------------------------
The TModalityList is used to store various types of information as TModalityObject
as specialised decendandants. They can finally be presented in one unified user-interface.
*)
{06/11/2016 new implementation}
{31/03/2022 added Info}
type
TModalityObject=class(TObject)
public
Modality,Info: String;
constructor Create(AModality:String='');
procedure Copy(ASource:TModalityObject); virtual;
destructor Destroy; override;
end;
twModalityArr = array of TModalityObject;
//modalities should be formatted with ModalityFormat -> twcModalityFormat = '%s%0.2f';
TModalityList=class(TObject)
private
FDataObj : twModalityArr;
FStatusProc: toExtMsgProc;
function GetModData(Index :Integer ): TModalityObject;
function GetModDataCount : Integer;
function GetCommaText(Index :Integer ): String; virtual;
function GetDivisorText(Index :Integer ): String;
procedure ExceptMessage(AString :String );
public
constructor Create(AStatusProc :toExtMsgProc =nil); virtual;
function ModalityFormat(AModality:twcModalityChar;
AEnergy :twcFloatType ): String;
function AddModData(ACommaText :String;
ASeparator :Char=',' ): Integer; virtual;
function DelModData(AModality :String;
AddedInfo :String='' ): Boolean; overload; virtual;
function DelModData(Index :Integer ): Boolean; overload; virtual;
function FindModData(AModality :String;
var RefObj:TModalityObject ): Boolean; overload; virtual;
function FindModData(AModality :String;
AddedInfo :String;
var RefObj:TModalityObject ): Boolean; overload; virtual;
function FindModData(AModality :String ): Integer; overload; virtual;
function FindModData(AModality :String;
AddedInfo :String ): Integer; overload; virtual;
function GetModalityList : String; virtual;
procedure SetStatusProcedure(AStatusProc:toExtMsgProc=nil);
procedure ClearModData;
destructor Destroy; override;
property DataObj[Index:Integer] : TModalityObject read GetModData;
property DataObjArray : twModalityArr read FDataObj;
property DataCount : Integer read GetModDataCount;
property CommaText[Index:Integer] : String read GetCommaText;
property DivisorText[Index:Integer]: String read GetDivisorText;
property StatusProcedure :toExtMsgProc read FStatusProc write SetStatusProcedure;
end;
//------modalitynorm---------------------------------------
(*
The twModNormRec contains two sets of normalisation points:
false: absolute normalisation point
true : relative normalisation point (for graph presentations)
The primairy index for searching is the modality/energy.
A norm record contains two lists to limit the applicability. When empty,
the list is ignored. On searching records with non-empty lists are preferred.
A non-resolvable caveat is that there may be overlapping definitions based on
field class and linac name.
AddModddata supports two fields with strings for field class names en linac names respectively.
Use an alternative separator within those strings or no separator at all.
*)
type
twModNormRec = record
Depth : array[False..True] of twcFloatType;
Value : array[False..True] of twcFloatType;
FT_list : set of twcFieldClass;
Linac_list: String;
end;
TModalityNorm=class(TModalityObject)
public
NormRec: twModNormRec;
constructor Create(AModality:String='' ); reintroduce;
procedure Copy(ASource :TModalityNorm); reintroduce;
destructor Destroy; override;
end;
TModNormList=class(TModalityList)
private
function GetCommaText(Index :Integer ): String; override;
public
constructor Create(AStatusProc :toExtMsgProc=nil ); override;
function AddModData(ACommaText :String;
ASeparator :Char =',' ): Integer; override;
function FindModData(AModality :String;
AFieldType:twcFieldClass;
ALinac :String): Integer; overload; reintroduce;
function FindModData(AModality :String;
AFieldType:twcFieldClass;
ALinac :String;
var RefObj:TModalityNorm ): Boolean; overload; reintroduce;
function GetModDepth(AModality :String;
AFieldType:twcFieldClass;
ALinac :String;
AbsDepth :Boolean =True;
ZeroValue :twcFloatType=0 ): twcFloatType;
function GetModValue(AModality :String;
AFieldType:twcFieldClass;
ALinac :String;
AbsValue :Boolean =True): twcFloatType;
destructor Destroy; override;
end;
//------modalityfilm---------------------------------------
type
twModFilmRec = record
FilmType: String;
OD2dose : twcOD2doseArray;
end;
TModalityFilm=class(TModalityObject)
public
FilmRec: twModFilmRec;
constructor Create(AModality:String=''); reintroduce;
procedure Copy(ASource:TModalityFilm); reintroduce;
destructor Destroy; override;
end;
TModFilmList=class(TModalityList)
private
function GetCommaText(Index :Integer ): String; override;
public
constructor Create(AStatusProc :toExtMsgProc=nil ); override;
function AddModData(ACommaText :String;
ASeparator :Char =',' ): Integer; override;
function DelModData(AModality :String;
AFilmType :String ): Boolean; reintroduce; overload;
function FindModData(AModality :String;
AFilmType :String;
var RefObj:TModalityFilm ): Boolean; reintroduce; overload;
function FindModData(AModality :String;
AFilmType :String ): Integer; reintroduce; overload;
function GetFilmTypeList : String;
destructor Destroy; override;
end;
//------modalitytext---------------------------------------
type
TModalityText=class(TModalityObject)
public
Value: String;
constructor Create(AModality:String='');
procedure Copy(ASource:TModalityText); reintroduce;
destructor Destroy; override;
end;
TModTextList=class(TModalityList)
private
function GetCommaText(Index :Integer ): String; override;
public
constructor Create(AStatusProc :toExtMsgProc=nil); override;
function AddModData(ACommaText :String;
ASeparator :Char =','): Integer; override;
function FindModData(AModality :String;
var RefObj:TModalityText ): Boolean; reintroduce;
function GetModValue(AModality :String ): String;
destructor Destroy; override;
end;
{supported datasources}
twcDataSource=twcFirstDataSource..twcLastDataSource; //range of supported sources
{======================== TRadthData profile base class =============================}
{24/08/2015 added ScanAngle}
{09/12/2015 added sharedparser}
{29/03/2016 added FRegisteredFiles}
{10/05/2016 added ErrorState}
{27/09/2016 added Binstream, binStreamType, binStreamFile, FBinaryAllowed}
{01/05/2020 added transfer of BinaryData in Create}
{16/05/2020 added FMultiScanCapable}
{13/10/2020 added AutoDecimalPoint,AutoDecimalList}
{19/10/2020 visibility of BinStream and BinsTreamFile extended to public}
{16/11/2020 added FindMoreData,ADataTopLine: support for multiple complete data sets in one single file intended for one single scan}
{14/01/2020 added PriorityMessage}
{30/03/2021 FormatOk dropped, use ParseOk instead}
TRadthData=class
protected
FExtraText : TStringDynArray;
FRegisteredFiles : String;
FBinaryAllowed : Boolean;
FMultiScanCapable: Boolean;
public
AutoDecimalPoint : Boolean;
AutoDecimalList : String;
BinStream : TMemoryStream;
BinStreamFile : String;
BinStreamType : twcFileType;
FileTime : TDateTime;
FileFormat : twcFileType;
UndefinedVal : twcFloatType;
UndefinedInt : wmsIntType;
PriorityMessage : String;
Linac : String;
IdentificationStg: String;
ScanAngle : twcFloatType; //CW angle from AB axis
ScanNr : Integer; //1-based
ScanNrOk : Integer;
ScanMax : Integer;
ShowWarning : Boolean;
IsFile : Boolean;
DefaultExtension : String;
ErrorState : Boolean;
ObjectCallSign : String;
{$IFDEF COMPILED_DEBUG}
FailInfo : String;
{$ENDIF}
constructor Create(SharedParser:toTNumParser =nil;
ParsedFile :String ='';
BinaryData :TMemoryStream=nil;
BinaryFile :String ='';
AStatusProc :toExtMsgProc =nil;
AIdentity :String ='base' );
procedure StatusMessage(AMessage :String;
UpdateLastMessage:Boolean =True;
MinLevel :ShortInt=1 );
function GetNumPoints : Integer; virtual;
function CheckData(AStringList:TStrings=nil ): Boolean; virtual;
function LoadBinStream(AFileName:String ): Boolean;
function ReadData(AStringList :TStrings;
ADataTopLine:Integer =0;
AFileFormat :twcFileType=twcUnknown): Boolean; overload; virtual;
function ReadData(AStream :TStream;
ADataTopLine:Integer =0;
AFileFormat :twcFileType=twcUnknown): Boolean; overload; virtual;
function ReadData(AFileName :String;
ADataTopLine:Integer =0;
AFileFormat :twcFileType=twcUnknown): Boolean; overload; virtual;
function ReadBinData : Boolean; virtual;
function WriteData(AFileName :String;
AStringList:TStrings;
ASource :twcDataSource=dsMeasured;
ClearList :Boolean =True ): Boolean; overload; virtual;
function WriteData(AFileName :String;
Binary :Boolean =True;
ASource :twcDataSource=dsMeasured;
SetExt :Boolean =True ): Boolean; overload; virtual;
function WriteData(AFileName :String;
OutPutType :twcFileType;
ASource :twcDataSource=dsMeasured;
SetExt :Boolean =True ): Boolean; overload; virtual;
destructor Destroy; override;
private
FIdentity : String;
FFileName : String;
FStatusProc : toExtMsgProc;
FLastMessage : String;
FLogLevel : Word;
FScanType : twcScanTypes;
FWarning : String;
FLocalParser : Boolean;
function DualReadData(AStringList :TStrings;
AStream :TStream;
AFileName :String;
ADataTopLine:Integer =0;
AFileFormat :twcFileType=twcUnknown ): Boolean; virtual;
function ParseData(CheckFileTypeOnly:Boolean=False ): Boolean; virtual;
function GetScanDirection(const ASide:twcSides ): twcMeasAxisStg;
function GetLastMessage : string;
function InsertIdentity(AMessage:String='' ): String;
procedure SetStatusProcedure(AStatusProc:toExtMsgProc=nil );
procedure SetLogLevel(ALevel:Word=1);
procedure TransferLastMessage(var AMessage:String );
procedure ExceptMessage(AString :String );
protected
FParser : toTNumParser;
FParserTopLine: Integer; //property ParserTopLine: offset to start of header, used to read consequtive single file structures into one file
FParseOk : Boolean;
procedure SetDefaults; virtual;
procedure AddWarning(AWarning:String); virtual;
function GetFileType(AFileName :String='';
BinaryOnly:Boolean=False ): twcFileType; virtual;
function GetFieldLength : twcFloatType; virtual;
function GetFieldDepth : twcFloatType; virtual;
function GetBeamType : twcBeamType; virtual;
function IsBinary(AFileName:String='' ): Boolean; virtual;
function ReadResults(PostText:String='' ): Boolean; virtual;
public
function FindMoreData(FromCurrentLine:Boolean=False): Boolean; virtual;
function GetDistance(c1,c2:twcCoordinate) : twcFloatType;
procedure ShiftPoint(var p :twcCoordinate;
const AShift:twcCoordinate );
published
property BeamType :twcBeamType read GetBeamType;
property Energy :twcFloatType read UndefinedVal;
property FieldDepth_cm :twcFloatType read getFieldDepth;
property FieldLength_cm :twcFloatType read GetFieldLength;
property FieldGT_cm :twcFloatType read UndefinedVal;
property FieldAB_cm :twcFloatType read UndefinedVal;
property FileName :String read FFileName write FFileName;
property Identity :String read FIdentity;
property LastMessage :String read GetLastMessage;
property LogLevel :word read FLogLevel write SetLogLevel;
property MultiScanCapable:Boolean read FMultiScanCapable;
property ParseOk :Boolean read FParseOk;
property Parser :toTNumParser read FParser;
property ParserTopLine :Integer read FParserTopLine; //offset to start of data header
property ScanType :twcScanTypes read FScanType;
property ScanLeftSide :twcMeasAxisStg index twcLeft read GetScanDirection;
property ScanRightSide :twcMeasAxisStg index twcRight read GetScanDirection;
property StatusProcedure :toExtMsgProc read FStatusProc write SetStatusProcedure;
property Warning :String read FWarning write AddWarning;
property WedgeAngle :twcFloatType read UndefinedVal;
end;
(*
-----RFA300 profile--------sample-------------------------------------
:MSR 1 # No. of measurement in file :MSR [n] identifying line
:SYS BDS 0 # Beam Data Scanner System :SYS BDS
#
# RFA300 ASCII Measurement Dump ( BDS format )
#
# Measurement number 1 #comment which shows meas number
#
%VNR 1.0 version type, should be 1.0
%MOD RAT modality, only RAT supported
[ FLM (Film) RAT (Ratio, Relative) ABS (Absolute) INT (Integrated) UDF (Undefined/Isodose) ]
%TYP SCN Type = identifies type of curve, only SCN supported here
[ SCN (Scan), ISO (Isodose), UDF (Undefined) ]
%SCN PRO ScanType = identifies type of scan.
[ DPT (DepthDose), PRO (Profile), MTX (Matrix), DIA (Diagonal), UDF (Undefined/Isodose) ]
%FLD ION DetectorType
[ ION (IonChamber), SEM (Semiconductor), UDF (Undefined) ]
%DAT 12-09-2011 dd-mm-yyyy
%TIM 13:26:36 hh:mm:ss
%FSZ 400 400 FieldWidth and FieldHeight in [mm]
%BMT PHO 6.0 RadType and energy [MV or MeV]
[ COB (Cobalt), PHO (Photons), ELE (Electrons), UDF (Undefined) ]
%SSD 1000 [mm]
%BUP 0 buildup in [0.1 mm]
%BRD 1000 BeamReferenceDist [mm]
%FSH -1 field shape
[ -1 (Undefined), 0 (Circular), 1 (Rectangular), 2 (Irregular) ]
%ASC 0 Accessory number
%WEG 0 wedge angle [degrees]
%GPO 0 GantryAngle [degrees]
%CPO 0 CollimatorAngle [degrees]
%MEA 2 MeasurementType
[ -1 (Undefined), 0 (Absolute dose), 1 (Open depth), 2 (Open profile),
4 (Wedge) , 5 (Wedge depth) , 6 (Wedge profile) ]
%PRD 500 ProfileDepth [0.1 mm]
%PTS 347 number of points
%STS 0.0 210.0 50.0 # Start Scan values in mm ( X , Y , Z )
%EDS 0.0 -210.0 50.0 # End Scan values in mm ( X , Y , Z )
!·CommentsLine1 max 60 char
!·CommentsLine2 max 60 char
#
# X Y Z Dose
#
= 0.0 210.0 50.0 99.9
= 0.0 209.1 50.0 99.5
= 0.0 207.4 50.0 99.6
******data order and directions are GT [mm], AB [mm], UP [mm], Dose [%]*****
no swapping needed
*)
{15/04/2022 FFF support introduced, based on general assumptions and experiences with other iba formats}
const
rfaNumMeasID = ':MSR';
rfaSystemID = ':SYS';
rfaMeasNumberID= '# Measurement number';
rfaModID = '%MOD';
rfaFileID = '%TYP';
rfaScanID = '%SCN';
rfaDetID = '%FLD';
rfaDateID = '%DAT';
rfaTimeID = '%TIM';
rfaFSizeID = '%FSZ';
rfaRadiationID = '%BMT';
rfaSSDID = '%SSD';
rfaBUPID = '%BUP';
rfaBRDID = '%BRD';
rfaFShapeID = '%FSH';
rfaAccessoryID = '%ASC';
rfaWedgeID = '%WEG';
rfaGantryID = '%GPO';
rfaCollimatorID= '%CPO';
rfaMeasID = '%MEA';
rfaDepthID = '%PRD';
rfaPointsID = '%PTS';
rfaStartScanID = '%STS';
rfaEndScanID = '%EDS';
rfaMeasInfoID = '!';
rfaDataID = '=';
rfaEndMeasID = ':EOM';
rfaEndFileID = ':EOF';
rfa_IDlen = 3;
rfaID = rfaNumMeasID;
type
rfa_ID =String[rfa_IDlen];
rfa_Header=record
rfaMSR: Integer;
rfaSYS: String;
end;
rfa_Data =record
rfaModType : rfa_ID; {%MOD}
rfaFileType : rfa_ID; {%TYP}
rfaScanType : rfa_ID; {%SCN}
rfaDetType : rfa_ID; {%FLD}
rfaDate : TDateTime; {%DAT, %TIM}
rfaField_mm : array[twcFieldSizeDesc] of Integer;{%FSZ}
rfaRadiation : rfa_ID; {%BMT}
rfaEnergy_MV : twcFloatType; {%BMT}
rfaFFF : Boolean; {"(FFF)" assumption, no proof in real world}
rfaSSD_mm : Integer; {%SSD}
rfaBUP_01mm : Integer; {%BUP}
rfaBRD_mm : Integer; {%BRD}
rfaFShape : Integer; {%FSH}
rfaAccessory : Integer; {%ASC}
rfaWedge_deg : Integer; {%WEG}
rfaGantry : Integer; {%GPO}
rfaCollimator : Integer; {%CPO}
rfaMeasType : Integer; {%MEA}
rfaDepth_01mm : twcFloatType; {%PRD}
rfaPoints : Integer; {%PTS}
rfaStart_Meas_cm : twcCoordinate; {%STS}
rfaEnd_Meas_cm : twcCoordinate; {%ETS}
rfaComments : array[0..1] of String; {!}
rfaCoordinates_cm: twcCoordArray; {=}
rfaValues : twcFloatArray; {=}
end;
{09/12/2015 added sharedparser}
{01/05/2020 added transfer of BinaryData in Create}
TRfaProfileData=class(TRadthData)
private
function ParseData(CheckFileTypeOnly:Boolean=False): Boolean; override;
public
RfaHeader : rfa_Header;
RfaData : rfa_Data;
constructor Create(SharedParser:toTNumParser =nil;
ParsedFile :String ='';
BinaryData :TMemoryStream=nil;
BinaryFile :String ='';
AStatusProc :toExtMsgProc =nil;
AIdentity :String='RFA300' ); reintroduce;
procedure SetDefaults; override;
function MakeTimeString(ADateTime:TDateTime ): String;
function GetFileType(AFileName :String ='';
BinaryOnly :Boolean=False): twcFileType; override;
function CheckData(AStringList :TStrings=nil ): Boolean; override;
function GetNumPoints : Integer; override;