2323)
2424from .utils import detect_depth_keys , get_header , open_file
2525
26+ # Trace sorting code (binary file header) for horizontally stacked data, i.e.
27+ # data that holds no gathers, such as a stack or a property model
28+ STACKED_SORTING = 4
29+
2630
2731@dataclass
2832class ShotRecord :
@@ -58,7 +62,7 @@ def __str__(self) -> str:
5862 f"{ self .coordinates [2 ]} "
5963 ")"
6064 )
61- lines .append (f" traces: { sum ( c for _ , c in self .segments ) } " )
65+ lines .append (f" traces: { self .ntraces } " )
6266 lines .append (f" ns: { self .ns } , dt: { self .dt } " )
6367 if self .summary :
6468 lines .append (" summary:" )
@@ -68,23 +72,58 @@ def __str__(self) -> str:
6872
6973 __repr__ = __str__
7074
71- def read_data (self , keys : Optional [Iterable [str ]] = None ) -> SeisBlock :
75+ @property
76+ def ntraces (self ) -> int :
77+ """
78+ Number of traces in this record.
7279 """
73- Load all traces for this shot.
80+ return sum (c for _ , c in self .segments )
81+
82+ def read_data (
83+ self ,
84+ keys : Optional [Iterable [str ]] = None ,
85+ traces : Optional [slice ] = None ,
86+ ) -> SeisBlock :
87+ """
88+ Load the traces of this shot.
89+
90+ Parameters
91+ ----------
92+ keys : Iterable[str], optional
93+ Header fields to load with each trace.
94+ traces : slice, optional
95+ Range of this record's traces to load. Only the corresponding part
96+ of the file is read; by default all traces are.
97+
98+ Returns
99+ -------
100+ ndarray
101+ ``ns`` x number of traces read.
74102 """
103+ ns = self .fileheader .bfh .ns
104+ trace_size = 240 + ns * 4
105+ start , stop , _ = (traces or slice (None )).indices (self .ntraces )
106+
75107 data_parts = []
108+ first = 0
76109 for offset , count in self .segments :
77- with open_file (self .path , "rb" , self .fs ) as f :
78- f .seek (offset )
79- _ , d = read_traces (
80- f ,
81- self .fileheader .bfh .ns ,
82- count ,
83- self .fileheader .bfh .DataSampleFormat ,
84- keys ,
85- )
86- data_parts .append (d )
87- return np .concatenate (data_parts , axis = 1 ) if data_parts else []
110+ # Part of this segment that falls within the requested range
111+ low , high = max (start , first ), min (stop , first + count )
112+ if high > low :
113+ with open_file (self .path , "rb" , self .fs ) as f :
114+ f .seek (offset + (low - first ) * trace_size )
115+ _ , d = read_traces (
116+ f ,
117+ ns ,
118+ high - low ,
119+ self .fileheader .bfh .DataSampleFormat ,
120+ keys ,
121+ )
122+ data_parts .append (d )
123+ first += count
124+ if not data_parts :
125+ return np .empty ((ns , 0 ), dtype = np .float32 )
126+ return np .concatenate (data_parts , axis = 1 )
88127
89128 def read_headers (
90129 self , keys : Optional [Iterable [str ]] = None
@@ -145,6 +184,59 @@ def rec_coordinates(self) -> np.ndarray:
145184 return self ._rec_coords
146185
147186
187+ def _is_gathered (fh : FileHeader , records : List [ShotRecord ]) -> bool :
188+ """
189+ Whether the scanned traces form gathers at all.
190+
191+ Post-stack data, a velocity model or any other property cube has no shot
192+ structure: the binary file header says so when its trace sorting is
193+ ``STACKED_SORTING``, and grouping such a file by source coordinate yields
194+ single-trace groups, since every trace then carries its own coordinate.
195+ """
196+ if fh .bfh .TraceSorting == STACKED_SORTING :
197+ return False
198+ return any (r .ntraces > 1 for r in records )
199+
200+
201+ def _merge_records (
202+ records : List [ShotRecord ], fh : FileHeader , fs = None
203+ ) -> ShotRecord :
204+ """
205+ Merge scanned records into a single one holding every trace, in file order.
206+ """
207+ trace_size = 240 + fh .bfh .ns * 4
208+ first = min (records , key = lambda r : r .segments [0 ][0 ])
209+
210+ segments : List [Tuple [int , int ]] = []
211+ for offset , count in sorted (s for r in records for s in r .segments ):
212+ if segments and offset == segments [- 1 ][0 ] + segments [- 1 ][1 ]* trace_size :
213+ segments [- 1 ] = (segments [- 1 ][0 ], segments [- 1 ][1 ] + count )
214+ else :
215+ segments .append ((offset , count ))
216+
217+ summary : Dict [str , Tuple [float , float ]] = {}
218+ for rec in records :
219+ for k , (mn , mx ) in rec .summary .items ():
220+ if k in summary :
221+ summary [k ] = (min (summary [k ][0 ], mn ), max (summary [k ][1 ], mx ))
222+ else :
223+ summary [k ] = (mn , mx )
224+
225+ return ShotRecord (
226+ first .path ,
227+ first .coordinates ,
228+ fh ,
229+ first .rec_depth_key ,
230+ first .depth_key ,
231+ first .by_receiver ,
232+ segments ,
233+ summary ,
234+ first .ns ,
235+ first .dt ,
236+ fs ,
237+ )
238+
239+
148240def _parse_header (buf : bytes , keys : Iterable [str ]) -> BinaryTraceHeader :
149241 """
150242 Return a :class:`BinaryTraceHeader` parsed from ``buf``.
@@ -308,7 +400,7 @@ def counts(self) -> List[int]:
308400 """
309401 Total number of traces for each shot.
310402 """
311- return [sum ( c for _ , c in r . segments ) for r in self .records ]
403+ return [r . ntraces for r in self .records ]
312404
313405 def __getitem__ (self , idx : int ) -> ShotRecord :
314406 """
@@ -332,43 +424,58 @@ def summary(self, idx: int) -> dict:
332424 return self .records [idx ].summary
333425
334426 def read_data (
335- self , idx : int , keys : Optional [Iterable [str ]] = None
427+ self ,
428+ idx : int ,
429+ keys : Optional [Iterable [str ]] = None ,
430+ traces : Optional [slice ] = None ,
336431 ) -> SeisBlock :
337432 """
338- Load all traces for a single shot.
433+ Load the traces of a single shot.
339434
340435 Parameters
341436 ----------
342437 idx : int
343438 Index of the shot to read.
344439 keys : Iterable[str], optional
345440 Additional header fields to load with each trace.
441+ traces : slice, optional
442+ Range of the shot's traces to load. Only the corresponding part of
443+ the file is read; by default all traces are.
346444
347445 Returns
348446 -------
349447 SeisBlock
350- In-memory representation of the selected shot .
448+ In-memory representation of the selected traces .
351449 """
352450 rec = self .records [idx ]
451+ ns = self .fileheader .bfh .ns
452+ trace_size = 240 + ns * 4
453+ start , stop , _ = (traces or slice (None )).indices (rec .ntraces )
454+
353455 headers : List [BinaryTraceHeader ] = []
354456 data_parts = []
457+ first = 0
355458 for offset , count in rec .segments :
356- fs_to_use = rec .fs if rec .fs is not None else getattr (self , "fs" , None )
357- with open_file (rec .path , "rb" , fs_to_use ) as f :
358- f .seek (offset )
359- h , d = read_traces (
360- f ,
361- self .fileheader .bfh .ns ,
362- count ,
363- self .fileheader .bfh .DataSampleFormat ,
364- keys ,
365- )
366- headers .extend (h )
367- data_parts .append (d )
459+ # Part of this segment that falls within the requested range
460+ low , high = max (start , first ), min (stop , first + count )
461+ if high > low :
462+ fs_to_use = rec .fs if rec .fs is not None else getattr (self , "fs" , None )
463+ with open_file (rec .path , "rb" , fs_to_use ) as f :
464+ f .seek (offset + (low - first ) * trace_size )
465+ h , d = read_traces (
466+ f ,
467+ ns ,
468+ high - low ,
469+ self .fileheader .bfh .DataSampleFormat ,
470+ keys ,
471+ )
472+ headers .extend (h )
473+ data_parts .append (d )
474+ first += count
368475 if data_parts :
369476 data = np .concatenate (data_parts , axis = 1 )
370477 else :
371- data = [] # pragma: no cover
478+ data = np . empty (( ns , 0 ), dtype = np . float32 )
372479 return SeisBlock (self .fileheader , headers , data )
373480
374481 def read_headers (
@@ -549,6 +656,10 @@ def _scan_file(
549656 records [previous ].segments .append ((seg_start , seg_count ))
550657
551658 record_list = sorted (records .values (), key = lambda r : r .coordinates )
659+ if record_list and not _is_gathered (fh , record_list ):
660+ vprint (f"{ thread } { path } holds no gathers, scanned as a single record" )
661+ record_list = [_merge_records (record_list , fh , fs )]
662+
552663 vprint (f"{ thread } found { len (record_list )} shots in { path } " )
553664 return SegyScan (fh , record_list , fs = fs )
554665
0 commit comments