Skip to content

Add spectrogram metadata classes#245

Open
Amityush-lgtm wants to merge 13 commits into
sunpy:ndcube-refactorfrom
Amityush-lgtm:metadata-classes
Open

Add spectrogram metadata classes#245
Amityush-lgtm wants to merge 13 commits into
sunpy:ndcube-refactorfrom
Amityush-lgtm:metadata-classes

Conversation

@Amityush-lgtm

@Amityush-lgtm Amityush-lgtm commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

PR Description

Fixes #243

Implementing a metadata class hierarchy for radiospectra, following the pattern used in sunraster and the LC Radio Workshop metadata requirements.
I tried to make some flowcharts as well, will attach them too.

Changes:

  • Added SpectrogramMetaABC and SpectrogramMeta (backed by NDMeta) in radiospectra/spectrogram/meta.py
  • Added instrument specific subclasses (WAVESMeta, SWAVESMeta, CALISTOMeta, EOVSAMeta, RSTNMeta)
  • Updated GenericSpectrogram to wrap raw dict metadata into SpectrogramMeta automatically
  • Added tests for the new meta class

AI Assistance Disclosure

AI tools were used for:

  • Code generation (e.g., when writing an implementation or fixing a bug)
  • Test/benchmark generation
  • Documentation (including examples)
  • Research and understanding
  • No AI tools were used

Regardless of AI use, the human contributor remains fully responsible for correctness, design choices, licensing compatibility, and long-term maintainability.

This is an initial draft, so I've just tried to make the changes in WAVES and CALISTO, if it looks in the right direction then I'll add the remaining subclasses also. Let me know your thoughts on this @samaloney and @hayesla.

@Amityush-lgtm

Amityush-lgtm commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Sharing the flowchart i used while designing this:

class_heirarchy_diagram (5)

Comment thread radiospectra/spectrogram/meta.py Outdated
# Identification
@property
@abc.abstractmethod
def instrument(self):

@samaloney samaloney Jun 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a short description and the type info should be encode here for each property e.g.

def instrument(self) -> str:
    """
    Name of the instrument
    """
    pass

and then here if we only return str then the default south be "" if that better or worse then returning a str or none e.g. -> str|None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for properties like date_start, frequency_rangeor data_units, what should i write for them??

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Start of the observation" Time
"Frequency range of observation" Quantity
"Unit for the data" Unit

Maybe we don't need the docstrings for them all but they types would be good

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more doubt, what would you prefer for observer_location and data_mask?

@samaloney

Copy link
Copy Markdown
Member

Also you can embed mermaid charts directly in GitHub doesn't match exactly the UML but close enough

classDiagram
    direction TB

    class NDMetaABC {
        <<ndcube.meta>>
    }

    class SpectrogramMetaABC {
        <<abstract>>
        +instrument
        +observatory
        +detector
        +date_start
        +date_end
        ...
    }

    class NDMeta {
        <<ndcube.meta>>
        dict subclass
        +axes
        ...
        +slice()
        +add()
        +rebin()
    }

    class SpectrogramMeta {
        <<concrete base>>
        dict-backed via NDMeta
        +instrument
        +observatory
        +detector
        +date_start
        +date_end
        ...
    }

    class WAVESMeta {
        +background
        +receiver
    }

    class CALISTOMeta {
        +observer_location
    }

    class OtherInstrumentMetas {
        <<...>>
    }

    %% Relationships
    SpectrogramMetaABC --|> NDMetaABC : inherits
    NDMeta --|> NDMetaABC : inherits

    SpectrogramMeta --|> NDMeta : inherits
    SpectrogramMeta ..|> SpectrogramMetaABC : implements

    WAVESMeta --|> SpectrogramMeta
    CALISTOMeta --|> SpectrogramMeta
    OtherInstrumentMetas --|> SpectrogramMeta
Loading

Comment thread radiospectra/spectrogram/meta.py Outdated
Comment on lines +84 to +88
@property
@abc.abstractmethod
def data_units(self) -> Unit:
"""Unit for the data"""
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is aleady cover by the unit in NDCube

Comment thread radiospectra/spectrogram/meta.py Outdated
Comment on lines +112 to +114
def observer_location(self):
"""Observer location."""
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe observe_coordinate would be better and this should be a SkyCoord | None

@Amityush-lgtm Amityush-lgtm Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I'm working on it

@Amityush-lgtm

Copy link
Copy Markdown
Contributor Author

Also you can embed mermaid charts directly in GitHub doesn't match exactly the UML but close enough

classDiagram
    direction TB

    class NDMetaABC {
        <<ndcube.meta>>
    }

    class SpectrogramMetaABC {
        <<abstract>>
        +instrument
        +observatory
        +detector
        +date_start
        +date_end
        ...
    }

    class NDMeta {
        <<ndcube.meta>>
        dict subclass
        +axes
        ...
        +slice()
        +add()
        +rebin()
    }

    class SpectrogramMeta {
        <<concrete base>>
        dict-backed via NDMeta
        +instrument
        +observatory
        +detector
        +date_start
        +date_end
        ...
    }

    class WAVESMeta {
        +background
        +receiver
    }

    class CALISTOMeta {
        +observer_location
    }

    class OtherInstrumentMetas {
        <<...>>
    }

    %% Relationships
    SpectrogramMetaABC --|> NDMetaABC : inherits
    NDMeta --|> NDMetaABC : inherits

    SpectrogramMeta --|> NDMeta : inherits
    SpectrogramMeta ..|> SpectrogramMetaABC : implements

    WAVESMeta --|> SpectrogramMeta
    CALISTOMeta --|> SpectrogramMeta
    OtherInstrumentMetas --|> SpectrogramMeta
Loading

Thats so cool, I didn't know GitHub supported Mermaid diagrams. I'll definitely try using them, Thanks a lot!

@Amityush-lgtm

Copy link
Copy Markdown
Contributor Author

Should i add the rest of the classes in this PR or like break it in different PRs??

@samaloney

Copy link
Copy Markdown
Member

I think adding the classes necessary to handle the instruments that are currently supported would make sense.

@Amityush-lgtm
Amityush-lgtm marked this pull request as ready for review July 3, 2026 19:03
@Amityush-lgtm

Copy link
Copy Markdown
Contributor Author

I have added the remaining classes, let know your thoughts on this @samaloney

@Amityush-lgtm
Amityush-lgtm requested a review from samaloney July 3, 2026 19:06

@hayesla hayesla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @Amityush-lgtm! this is looking good (sorry for delay)

I think there are some issue here, I think it would also be good to make some tests making some generic wrapping itself, for example a test that constructs a GenericSpectrogram from a dict and assert the meta etc, and that the things like spec.observatory/start_time/etc return the right types?

it is probably also good to check that meta survives slicing (i.e. if you slice the Spectrogram does the meta slice?), somethign worth checking! this may also be good to do in another PR

return self.get("frequency_resolution")

@property
def calibration_state(self) -> str | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I wonder if calibration_state is the right word here? could it just be calibrated and then have boolean? @samaloney would there ever be a case where the data is partially calibrated?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷 I'm not sure if this isn't redundant with level but maybe that's ok?

"""Metadata for WIND/WAVES spectrograms."""

@property
def background(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this here? only on WAVES?

@Amityush-lgtm Amityush-lgtm Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

background is only used by WAVES, i added it on WAVESMeta to make it easy to find. Should i move it in the base class ??

"""Metadata for PSP FIELDS/RFS spectrograms."""

@property
def level(self) -> str | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the processing_level no?

@Amityush-lgtm Amityush-lgtm Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is processing_level but since it will override from the base class, thats why i changed its name. I've updated it to override.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the point is we map different convention e.g. name back to our "standard" name.

Comment on lines 44 to +48
"""
The name of the observatory which recorded the spectrogram.
"""
return self.meta["observatory"].upper()
val = getattr(self.meta, "observatory", self.meta.get("observatory"))
return val.upper() if val else None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm there's an issue with this, this getattr finds the observatory property on SpectrogramMeta, and so the .get() fallback here is never used - so there is actually a bug with a KeyError being raised is observatory isnt passed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to simplify these to just use self.meta.get() directly for fallback

"""Metadata for e-CALLISTO spectrograms."""

@property
def observer_coordinate(self) -> SkyCoord | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this actually returns an EarthLocation not a SkyCoord - i think we should actually use a SkyCoord (i.e. convert the EarthLocation to a SkyCoord) i think this can be done with .get_gcrs(obstime)

Comment thread radiospectra/spectrogram/meta.py Outdated

@property
def date_start(self) -> Time:
return self["start_time"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be

Suggested change
return self["start_time"]
return Time(self["start_time"])
```?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like the -> Time annotation only holds if the caller stored a Time?

return self["start_time"]

@property
def date_end(self) -> Time:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here

{
"processing_level": "L2",
"version": "1.0",
"wavelength": "radio",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wavelength here shouldnt be a string? (i realise this is just a test but its not right)

return self.get("background")

@property
def receiver(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this specific to waves? it could be on base class?

super().__init__(meta=meta, data=data, **kwargs)

@property
def receiver(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment below about this maybe being on base class

@Amityush-lgtm

Copy link
Copy Markdown
Contributor Author

I've pushed the latest changes with all the suggested updates, let me know if i've missed anything or anything else to improve

@Amityush-lgtm
Amityush-lgtm requested a review from hayesla July 14, 2026 07:14
@Amityush-lgtm Amityush-lgtm changed the title Initial draft of SpectrogramMeta classes Add spectrogram metadata classes Jul 17, 2026
@hayesla hayesla added the NDCube Refactor This label is for issues/PR related to NDCube refactor for GSOC project 2026 label Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

NDCube Refactor This label is for issues/PR related to NDCube refactor for GSOC project 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants