diff --git a/geomagio/Controller.py b/geomagio/Controller.py index 0985597c37f0e3ff6912b03eaceb009a6ad0ab96..4e212579a625d08143fbf64bce5979c40976080c 100644 --- a/geomagio/Controller.py +++ b/geomagio/Controller.py @@ -7,7 +7,7 @@ from typing import List, Optional, Tuple, Union from obspy.core import Stream, UTCDateTime -from geomagio.ImagCDFFactory import ImagCDFFactory +from geomagio.imagcdf.ImagCDFFactory import ImagCDFFactory from .algorithm import Algorithm, algorithms, AlgorithmException, FilterAlgorithm from .DerivedTimeseriesFactory import DerivedTimeseriesFactory @@ -28,7 +28,7 @@ from . import vbf from . import xml from . import covjson from . import netcdf -from geomagio.ImagCDFFactory import ImagCDFFactory +from geomagio.imagcdf.ImagCDFFactory import ImagCDFFactory class Controller(object): @@ -635,7 +635,7 @@ def get_output_factory(args): elif output_type == "plot": output_factory = PlotTimeseriesFactory() elif output_type == "imagcdf": - output_factory = ImagCDFFactory() + output_factory = ImagCDFFactory(**output_factory_args) else: # stream compatible factories if output_type == "binlog": diff --git a/geomagio/ImagCDFFactory.py b/geomagio/imagcdf/ImagCDFFactory.py similarity index 93% rename from geomagio/ImagCDFFactory.py rename to geomagio/imagcdf/ImagCDFFactory.py index af603201de1773abdfcbe65836f290142e28d590..a7838f14fb8283ae15d138bee61807af200efeba 100644 --- a/geomagio/ImagCDFFactory.py +++ b/geomagio/imagcdf/ImagCDFFactory.py @@ -25,10 +25,10 @@ from sqlalchemy import true from geomagio.TimeseriesFactory import TimeseriesFactory from geomagio.api.ws.Element import TEMPERATURE_ELEMENTS_ID -from .geomag_types import DataInterval, DataType -from .TimeseriesFactoryException import TimeseriesFactoryException -from . import TimeseriesUtility -from . import Util +from ..geomag_types import DataInterval, DataType +from ..TimeseriesFactoryException import TimeseriesFactoryException +from .. import TimeseriesUtility +from .. import Util import cdflib from cdflib.cdfwrite import CDF as CDFWriter @@ -93,6 +93,7 @@ class ImagCDFFactory(TimeseriesFactory): """ isUniqueTimes = True # used to determine depend_0 and CDF Time Variable Name + NONSTANDARD_ELEMENTS = TEMPERATURE_ELEMENTS_ID def __init__( self, @@ -123,13 +124,6 @@ class ImagCDFFactory(TimeseriesFactory): urlInterval=urlInterval, ) - def parse_string(self, data: str, **kwargs): - """Parse ImagCDF formatted string data into a Stream. - - Note: Parsing from strings is not implemented in this factory. - """ - raise NotImplementedError('"parse_string" not implemented') - def write_file(self, fh, timeseries: Stream, channels: List[str]): # Create a temporary file to write the CDF data with tempfile.NamedTemporaryFile(delete=False, suffix=".cdf") as tmp_file: @@ -164,7 +158,7 @@ class ImagCDFFactory(TimeseriesFactory): "Rec_Vary": True, "Var_Type": "zVariable", "Dim_Sizes": [], - "Sparse": "no_sparse", # no_sparse because there should not be time gaps. + "Sparse": "no_sparse", # no_sparse because there should not be time gaps. (Time stamps must represent a regular time series with no missing values in the series) "Compress": 9, "Pad": None, } @@ -178,11 +172,10 @@ class ImagCDFFactory(TimeseriesFactory): temperature_index = 0 for trace in timeseries: channel = trace.stats.channel - if channel in TEMPERATURE_ELEMENTS_ID: - temperature_index += 1 # MUST INCREMENT INDEX BEFORE USING - var_name = f"Temperature{temperature_index}" - else: - var_name = f"GeomagneticField{channel}" + var_name = f"GeomagneticField{channel}" + # if channel in REAL_TEMPERATURE: + # temperature_index += 1 # MUST INCREMENT INDEX BEFORE USING + # var_name = f"Temperature{temperature_index}" data_type = self._get_cdf_data_type(trace) num_elements = 1 if data_type in [ @@ -258,8 +251,8 @@ class ImagCDFFactory(TimeseriesFactory): observatory = stats.station starttime = starttime or stats.starttime endtime = endtime or stats.endtime - # Split data into intervals if necessary + urlIntervals = Util.get_intervals( starttime=starttime, endtime=endtime, size=self.urlInterval ) @@ -295,7 +288,6 @@ class ImagCDFFactory(TimeseriesFactory): starttime=interval_start, endtime=interval_end, ) - # Check if the file already exists to merge data if os.path.isfile(url_file): try: @@ -325,13 +317,13 @@ class ImagCDFFactory(TimeseriesFactory): ) # Proceed with new data - # Pad the data with NaNs to ensure it fits the interval + # Pad the data to ensure it fits the interval url_data.trim( starttime=interval_start, endtime=interval_end, nearest_sample=False, pad=True, - fill_value=np.nan, + fill_value=99_999, # FILLVAL ) # Write the data to the CDF file @@ -539,6 +531,7 @@ class ImagCDFFactory(TimeseriesFactory): def _create_time_stamp_variables(self, timeseries: Stream) -> dict: vector_times = None scalar_times = None + nonstandard_times = None temperature_times = {} temperature_index = 1 @@ -571,14 +564,21 @@ class ImagCDFFactory(TimeseriesFactory): raise ValueError( "Time stamps for scalar channels are not the same." ) - elif channel in TEMPERATURE_ELEMENTS_ID: - ts_key = f"Temperature{temperature_index}Times" - if ts_key not in temperature_times: - temperature_times[ts_key] = tt2000_times - temperature_index += 1 + # elif channel in REAL_TEMPERATURES: + # ts_key = f"Temperature{temperature_index}Times" + # if ts_key not in temperature_times: + # temperature_times[ts_key] = tt2000_times + # temperature_index += 1 + # else: + # temperature_times[ts_key] = tt2000_times + elif channel in self.NONSTANDARD_ELEMENTS: + if scalar_times is None: + nonstandard_times = tt2000_times else: - temperature_times[ts_key] = tt2000_times - + if not np.array_equal(scalar_times, tt2000_times): + raise ValueError( + "Time stamps for nonstandard channels are not the same." + ) time_vars = {} if vector_times is not None: time_vars["GeomagneticVectorTimes"] = vector_times @@ -587,16 +587,28 @@ class ImagCDFFactory(TimeseriesFactory): if temperature_times: time_vars.update(temperature_times) + isNonStandard = nonstandard_times is not None + if isNonStandard: + time_vars["NonstandardTimes"] = nonstandard_times + last_times = [] + self.isUniqueTimes = ( len(time_vars) == 1 - ) # true if only one set of times, else default to false. + ) # (true if is a single time stamp variable in the file) for index, times in enumerate(time_vars.values()): if index > 0: self.isUniqueTimes = not np.array_equal(last_times, times) last_times = times - - return time_vars if self.isUniqueTimes else {"DataTimes": last_times} + if self.isUniqueTimes and isNonStandard and len(time_vars) > 2: + raise ValueError( + f"Time stamps must be the same for all channels when using nonstandard channels: {','.join(self.NONSTANDARD_ELEMENTS)}" + ) + return ( + {"DataTimes": last_times} + if isNonStandard or not self.isUniqueTimes + else time_vars + ) def _create_var_spec( self, @@ -660,9 +672,13 @@ class ImagCDFFactory(TimeseriesFactory): validmax = 90.0 # The magnetic field vector can point straight down (+90°), horizontal (0°), or straight up (-90°). elif channel in TEMPERATURE_ELEMENTS_ID: units = "Celsius" - fieldnam = f"Temperature {temperature_index} {trace.stats.location}" validmin = -273.15 # absolute zero validmax = 79_999 + # elif channel in [REAL_TEMPERATURES]: + # units = "Celsius" + # fieldnam = f"Temperature {temperature_index} {trace.stats.location}" + # validmin = -273.15 # absolute zero + # validmax = 79_999 elif channel in ["F", "S"]: units = "nT" validmin = ( @@ -675,12 +691,14 @@ class ImagCDFFactory(TimeseriesFactory): validmax = 79_999.0 # Determine DEPEND_0 based on channel type - if channel in self._get_vector_elements(): + if not isUniqueTimes: + depend_0 = "DataTimes" + elif channel in self._get_vector_elements(): depend_0 = "GeomagneticVectorTimes" elif channel in self._get_scalar_elements(): depend_0 = "GeomagneticScalarTimes" - elif channel in TEMPERATURE_ELEMENTS_ID: - depend_0 = f"Temperature{temperature_index}Times" + # elif channel in REAL_TEMPERATURES: + # depend_0 = f"Temperature{temperature_index}Times" var_attrs = { "FIELDNAM": fieldnam, @@ -688,10 +706,10 @@ class ImagCDFFactory(TimeseriesFactory): "FILLVAL": 99999.0, "VALIDMIN": validmin, "VALIDMAX": validmax, - "DEPEND_0": depend_0 if isUniqueTimes else "DataTimes", + "DEPEND_0": depend_0, "DISPLAY_TYPE": "time_series", "LABLAXIS": channel, - "DATA_INTERVAL_TYPE": trace.stats.data_interval_type, + "DATA_INTERVAL_TYPE": trace.stats.data_interval_type, # optional } return var_attrs @@ -953,10 +971,11 @@ class ImagCDFFactory(TimeseriesFactory): # If the urlTemplate doesn't support placeholders, assume 'file://' scheme if self.urlTemplate.startswith("file://"): - base_path = self.urlTemplate[7:] # Strip "file://" + base_path = self.urlTemplate[7:] if not base_path or base_path == "{obs}_{dt}_{t}.cdf": base_path = os.getcwd() # Default to current working directory - return os.path.join(base_path, filename) + return os.path.join(base_path, filename) + return os.path.join(self.urlTemplate, filename) # Unsupported URL scheme raise TimeseriesFactoryException(