Digitized Sky Survey POSS-1

I now have a repository that will hammer Caltech's servers download and save the files of interest.

I'm converting all the fits files to normalize, 8 bit Png files so it does lose alot of scientific value. However, this massively reduces the data size. Nearly by a factor of 10 from my estimates and I still have plenty detail for what I plant to do (or at least I think I do).
Haven't read or tried the code but just wanted to say it's awesome you did this!
 
Haven't read or tried the code but just wanted to say it's awesome you did this!
To be fair, I've hardly read the code either.
I had Gemini write most of it. Seems to work well enough though.

The smallest image I have is 72mb which is too large for the forum, otherwise I'd post it.
 
Haven't read or tried the code but just wanted to say it's awesome you did this!
I also have more planned.

I want to try find transients/defects using slightly different methods than they did in the paper. Not quite certain how I'll do that quite yet.
I'm bouncing between pure pixel comparison and blob detection.
 
If I simply use the `attach files button` instead of dragging it into the text box, it works.

I also had to change my scaling method so it would be more robust.
 

Attachments

  • 07G5_XE002.png
    07G5_XE002.png
    2.2 MB · Views: 95
It's not incredibly small relative to LEO in terms of area. It's small as a percentage of the total area of the sphere at that distance.
Apologies; that is what I meant. The shadowed area of the sky is very small at GEO, 0.56% of the sky.

Note as well that the only satellites in GEO which appear to be stationary are those in equatorial orbits; all the other ones would display some sort of north/south movement at various times, so the glints should be elongated most of the time.
 
Apologies; that is what I meant. The shadowed area of the sky is very small at GEO, 0.56% of the sky.

Note as well that the only satellites in GEO which appear to be stationary are those in equatorial orbits; all the other ones would display some sort of north/south movement at various times, so the glints should be elongated most of the time.
Not quite. They are considered stationary with respect to a point on the surface of the Earth not against the backdrop of stars. To do that they orbit at the rate of the Earth's rotation, circling the visible sky once each day. That's how people in earlier posts are deriving the expected change in position on the photographic plates.
 
Thanks!
Indeed, it seems that no true satellites could ever be observed as stationary against the distant stars, so there should be some sort of elongation in every case. I'm fairly sure that any glint of more than a small fraction of a second would show elongation, but Villarroel seems to be claiming that there are hundreds, or thousands of them.

A glint of a fraction of a second would need to be incredibly bright to register on a 50 minute exposure.
 
Not quite. They are considered stationary with respect to a point on the surface of the Earth not against the backdrop of stars.
I'm confused. The satellite goes around once a day, which is also how fast the stars go around... yeah, the stars change with the seasons, while the satellite would not, but would THAT minor a change over the course of a photo session be enough to show up? Or am I missing something?
 
I'm confused. The satellite goes around once a day, which is also how fast the stars go around.
Geostationary satellites don't go around.

Or rather, stars don't go around, they just look like they do because the Earth is rotating. Geos go around at the same angular speed as the Earth is "going around"

So stars and satellites always seem to move relative to each other in the sky.
 
A bit under one degree per day so enough for professional astronomers to notice in 20-40 minute exposures.

I'm embarrassed to say it but Google Gemini did a better summary than I did above.

External Quote:
"On average, geosynchronous satellites appear to remain at a fixed position in the sky relative to an observer on Earth, although they may exhibit a figure-eight or oscillating motion depending on the specific orbit's characteristics.
However, the Earth rotates once every 23 hours, 56 minutes, and 4 seconds (a sidereal day), not a full 24 hours. Because of this, stars appear to move about one degree further west every 24 hours, meaning in a day, stars appear to complete a full 361-degree rotation rather than 360 degrees.
Therefore, geosynchronous satellites move 360 degrees per day with respect to the Earth's rotation, but with respect to the celestial background (the stars), they actually move about 361 degrees in a 24-hour period, effectively gaining about one degree each day compared to a star that returns to the same spot in 23 hours and 56 minutes. This difference adds up over the year, so different star groups appear at different times of the night as the seasons change. "
 
With respect to the fixed stars, geostationary satellites move about 361 degrees every day.

That is much slower than LEO satellites, which move about one and a half degrees per second, if they are at the same height as the ISS. A lot of LEO satellites are higher than the ISS, since they don't want to hit it. The higher they are, the slower they orbit (as seen from the surface).
 
Geostationary satellites don't go around.

Or rather, stars don't go around, they just look like they do because the Earth is rotating. Geos go around at the same angular speed as the Earth is "going around"

So stars and satellites always seem to move relative to each other in the sky.
Oh, of course, I was visualizing things quite incorrectly. I'm up to speed now... The stars (seem to) rise and set like the Sun and moon, the gestationary satellite would not.

Thanks.
 
It's not incredibly small relative to LEO in terms of area. It's small as a percentage of the total area of the sphere at that distance.

This graph should show how the area shrinks in absolute terms (orange dashed) vs. relative to hemisphere area (blue). It's ChatGPT, but seems correct.

View attachment 83186

At GEO it's 1% of the hemisphere, 0.5% of the total sky.

or 0.7%, so a surplus rather than a defict.

But they say:

External Quote:



To independently verify the number of transients located within Earth's shadow, we implemented a custom code
(using ChatGPT-assisted scripting) that follows a similar principles to EarthShadow. After validating its performance
on a subset of candidates from Villarroel et al. (2025), we applied it to the full sample. The resulting counts — 374
transients at 42,164 km and 57 at 80,000 km — are in good agreement with the results obtained using EarthShadow,
supporting the robustness of our shadow deficit measurement.

To estimate the statistical significance of the difference in transient detection rates within Earth's umbra at different421
altitudes, we compute Poisson uncertainties for the observed and expected fractions. At 42,164 km altitude, we expect

N = 1223 transients in shadow out of 106,339 total, corresponding to an expected fraction of fexp = 0.0115 ±0.00033.
However, we observe only N = 349 transients in shadow, yielding fobs = 0.00328 ±0.00018. The difference between
these fractions is highly significant, with a significance level of 21.9σ, computed by combining the Poisson uncertainties
1223/106339 is 1.15%
349/106339 is 0.33%

Oh, they use geocentric radius (a sphere defined by the radius from the center of the earth), not altitude (a sphere defined by it's altitude above a nominally spherical Earth). So 42,164 is GEO.

I've come to think there is a strong spatial-temporal collection bias that needs to be accounted for (at least in the 5,399). Would it be a valid test to first identify all plates that the 5,399 sources were drawn from, and then calculate the percentage of those plates (assuming they searched the whole plate) that would correspond to shadow at GEO?
 
Last edited:
I've come to think there is a strong spatial-temporal collection bias that needs to be accounted for (at least in the 5,399). Would it be a valid test to first identify all plates that the 5,399 sources were drawn from, and then calculate the percentage of those plates (assuming they searched the whole plate) that would correspond to shadow at GEO?
My thought would be to grab random points on every plate and see how many actually end up in the shadow.
 
My thought would be to grab random points on every plate and see how many actually end up in the shadow.
Good advice. @beku-mant Use random sampling to reduce the sample size if the full size would be too insane. I'm still not familiar with the plates and everything though, so look into using stratified random sampling if it fits the problem. (Sorry I'm not being very helpful here yet :(). In fact, can you write out a complete list of your analysis plan? Doesn't have to be super detailed.

When you say,
I've come to think there is a strong spatial-temporal collection bias
You're saying that you think they tended to collect the data at particular times, from a specific area of the sky, rather than a more wide ranging approach across times and areas of the sky?
 
Villarroel's earlier paper, A glint in the eye: Photographic plate archive searches for non-terrestrial artefacts, suggests the reason to look in geosynchronous orbits is that if an extraterrestrial civilization had sent probes to look at earth at some point in the distant past those are the only orbits that would be stable over many thousands of years and where the objects could still be found. (This is not backed up by any calculations about the local stability of such orbits, just a reference to an astrophysicist suggesting that one might be able to detect an alien civilization on an exoplanet by the optical effects of an orbiting ring of space junk.)

Though the paper discusses reflectivity, it insists that that it can only result from manufactured objects and that while reflective materials would degrade from micrometeorite impact and radiation "it is reasonable to assume that an extraterrestrial civilisation that launches a probe to the Earth will have developed materials and systems that could endure space travel of up to millions of years."

Which was immediately followed by the contradictory: "The degrading of material due to micrometeorites and cosmic radiation opens up a window of new possibilities: were one to make a mission to the geosynchronous orbits to collect the debris, it is almost trivial to identify debris that has been there for thousands of years by looking for objects having the most micrometeorite hits and largest loss of reflectivity of its surface." (Though if I found and recovered an obviously non-human piece of technology in Earth orbit I doubt my first question would be about its age in thousands of years.)

This is followed by a long discussion of glints and the "how to recognise signs of artificial objects in the pre-satellite images" and the claim "The smoking-gun observation that settles the question unequivocally, is the one of repeating glints with clear PSFs [Point Source Function shapes] along a straight line in a long-exposure image."
 
We need a new emoji for "helpful but the content is enraging".
Though the paper discusses reflectivity, it insists that that it can only result from manufactured objects and that while reflective materials would degrade from micrometeorite impact and radiation "it is reasonable to assume that an extraterrestrial civilisation that launches a probe to the Earth will have developed materials and systems that could endure space travel of up to millions of years."
OMG come on!!! You can't just write that shit in scientific papers and expect to be taken seriously. The publication, Acta Astronautica, does peer review, but does anyone know if every submission is peer reviewed?

From wiki:
External Quote:
Which was immediately followed by the contradictory: "The degrading of material due to micrometeorites and cosmic radiation opens up a window of new possibilities: were one to make a mission to the geosynchronous orbits to collect the debris, it is almost trivial to identify debris that has been there for thousands of years by looking for objects having the most micrometeorite hits and largest loss of reflectivity of its surface." (Though if I found and recovered an obviously non-human piece of technology in Earth orbit I doubt my first question would be about its age in thousands of years.)
This is the most amateur writing I have ever seen in a legitimate journal. It sounds like a personal blog.
This is followed by a long discussion of glints and the "how to recognise signs of artificial objects in the pre-satellite images" and the claim "The smoking-gun observation that settles the question unequivocally, is the one of repeating glints with clear PSFs [Point Source Function shapes] along a straight line in a long-exposure image."
alsngfoianwoiegnawoigawigjoawejgligmlwekgoiwjoeijowneoi (sorry just my mind exploding reading this)
 
My thought would be to grab random points on every plate and see how many actually end up in the shadow.
That seems to be what they do, for the "more correct" calculation I mentioned earlier. I should have quoted more.

From "2025 Aligned, multiple-transient events in the First Palomar Sky Survey"

External Quote:

We performed an additional test to estimate the actual fraction of the survey sky that was covered by Earth's
shadow during the actual POSS-I observations, and to compare it to the actual observed fraction of transients falling
within the shadow. The transient sample is based on 635 unique photographic plates, each with a designated central
coordinate (RA, Dec) in J2000 and a corresponding observation time. Each plate spans 6 × 6 degrees on the sky, as
listed by STScI. We simulated 180 random points per plate, for a total of 114,300 points. For each simulated point,
we tested whether it would fall within Earth's shadow at a geosynchronous altitude (42,164 km) during a 50-minute
exposure starting from the recorded observation.443
Out of the 114 300 simulated points (180 points per plate), 610 were found to lie within Earth's shadow, implying
that approximately 0.53% of the survey area should be shadowed at GSO. However, in our actual transient dataset,
only 349/107875, 0.32% of the events occur within the shadow, corresponding to a∼39% deficit, significant at the 7.6σ
level. We repeated the same procedure at a higher altitude of 80 000 km. In this case, the actual shadow coverage
drops to (109 / 114300) 0.1%, while the observed fraction of transients (76/107875) within the shadow is only 0.07%
— a ∼26% deficit, significant at the 2σ level. The simulated points within each plate are used solely to estimate
the expected geometric coverage of the Earth's shadow during the exposure time, and are not meant to represent the
spatial distribution of actual transients.
This also suggests that a larger fraction of objects may be located near GSO
than at 80 000 km, although the limited number of events at 80,000 km makes the comparison statistically uncertain.
 
Good advice. @beku-mant Use random sampling to reduce the sample size if the full size would be too insane. I'm still not familiar with the plates and everything though, so look into using stratified random sampling if it fits the problem. (Sorry I'm not being very helpful here yet :(). In fact, can you write out a complete list of your analysis plan? Doesn't have to be super detailed.

When you say,

You're saying that you think they tended to collect the data at particular times, from a specific area of the sky, rather than a more wide ranging approach across times and areas of the sky?
Each fits file for the 5,399 sources has a plate id, plate center coordinates, and plate size, and other info. Couldn't you just extract that plate info for the set of plates a source is in, and then randomly generate positions within the coordinate spaces of these plate, but using the real plate obs date and time?

The approach I started but haven't followed through with, is to perform the normal computation, but randomize the coords within the same plate for a number of trials. Like this, but randomizing the plate coords instead of time.

By the way, randomizing the time results in about 0.53% of coords in shadow at GEO which seems to agree with the naive percent of GEO shell in shadow approach, but that just reveals a strong spatiotemporal bias I think.

Python:
import os
import glob
import numpy as np
import matplotlib.pyplot as plt
from astropy.io import fits
from astropy.time import Time
from earthshadow import get_shadow_center, get_shadow_radius, dist_from_shadow_center
from astropy.coordinates import SkyCoord
import astropy.units as u
from tqdm import tqdm

def time_str(date_obs, ut):
    date_split = date_obs.split("/")
    return f"19{date_split[2]}-{date_split[1]}-{date_split[0]} {ut}"

def in_shadow(ra_value, dec_value, time):
    center = get_shadow_center(time, obs='Palomar', orbit='GEO')
    radius = get_shadow_radius(orbit='GEO')
    dist = dist_from_shadow_center(ra_value, dec_value, time=time, obs='Palomar', orbit='GEO')
    return dist < radius - 2 * u.deg

<TODO>
def randomize_plate_coord( <needed info> ):
    ...

def extract_data(fits_files):
    extracted_data = []
    for fits_file in fits_files:
        with fits.open(fits_file) as hdul:
            header = hdul[0].header
            c = SkyCoord(header["OBJCTRA"], header["OBJCTDEC"], unit=(u.hourangle, u.deg), frame='icrs')
            ra_deg  = c.ra.degree
            dec_deg = c.dec.degree
            t = Time(time_str(header["DATE-OBS"], header["UT"]), scale="utc", format="iso")
            extracted_data.append({'ra': ra_deg, 'dec': dec_deg, 'time': t})
    return extracted_data

def run_trial( data, randomize=False):
    results = []
    for obs in data:
        t = randomize_time(obs['time']) if randomize else obs['time']
        result = in_shadow(obs['ra'], obs['dec'], t)[ 0 ]
        results.append(result)

    return results

if __name__ == "__main__":

    N = 100

    fits_files = glob.glob('fits_files/*.fits')
    extracted_data = extract_data(fits_files)
    
    results_real = run_trial(extracted_data, randomize=False)
    real_percent = np.mean(results_real) * 100

    rand_percents = []
    for i in tqdm(range(N), desc="Running trials"):
        random_results = run_trial(extracted_data, randomize=True)
        rand_percents.append(np.mean(random_results) * 100)

    rnd_percent_mean = np.mean(rand_percents)

    ####################################################################
    
    plt.style.use('seaborn-v0_8-whitegrid')
    fig, ax = plt.subplots(figsize=(10, 6))

    ax.hist(rand_percents, bins=8, color='skyblue', edgecolor='black', alpha=0.7, label='Distribution of Random Trial Percentages')
    ax.axvline(x=real_percent, color='firebrick', linestyle='--', linewidth=2.5, label=f'Real Percentage: {real_percent:.1f}%')
    ax.axvline(x=rnd_percent_mean, color='green', linestyle='-', linewidth=2.5, label=f'Random Trial Mean: {rnd_percent_mean:.1f}%')

    ax.set_title(f'Distribution of Overall Percent in Shadow for {N} Randomized Trials', fontsize=16)
    ax.set_xlabel('Overall Percentage In Earth\'s Shadow (%)', fontsize=12)
    ax.set_ylabel('Number of Trials', fontsize=12)
    ax.legend(fontsize=10)
    
    plt.tight_layout()
    plt.show()
 
When you say,

You're saying that you think they tended to collect the data at particular times, from a specific area of the sky, rather than a more wide ranging approach across times and areas of the sky?
The bias could come from things like scheduling when/where they pointed the telescope when producing the original images, what spatial coverage the plates have (I haven't checked, is it the whole sky), in addition to whether VASCO searched all plates from POSSI uniformly, etc.?

I.e. collection bias could propegate up from the choices made when capturing the original images, all the way up to extracting the 5,399 sources.
 
For each simulated point, we tested whether it would fall within Earth's shadow at a geosynchronous altitude (42,164 km) during a 50-minute
exposure starting from the recorded observation.
This is a crucial thing I haven't figured out how to account for yet. Does anyone know the proper way to do this? I have DATE_OBS, and UT corresponding to the date and time I guess when the exposure period started. But a point on the plate could be in shadow for a portion of the exposure time.

What if you just randomize the coordinate within the plate, and randomize the time within the 50 minute exposure window? I guess you also have the unknown duration of the glint assuming it is a glint.
 
Last edited:
Um; the Earth's shadow at GSO is only 70 minutes long, at most. That is to say, a satellite in that orbit would pass through the shadow in 70 minutes.
From here
https://www.metabunk.org/threads/tr...servatory-sky-survey.14362/page-3#post-349902
From Figure 3 we can also see that because of this sun-earth geometry, the geostationary orbit is usually outside the cone of the earth's shadow. That is, until around the times of the vernal and autumnal equinoxes (the beginning of spring and fall). At these times, geostationary satellites enter their eclipse season, when they can spend as much as 70 minutes of every day in shadow. These seasons run from the end of February through the middle of April and the beginning of September through the middle of October.

Even a location that is in shadow at one point during a 50 minute exposure would be quite likely to be in full sunlight at a different time during the same exposure, so you can't say with confidence 'this location was in shadow when the photo was taken'.
 
So I had Claude Sonnet attempt to make a python script to find the percentage of the total plate area eclipsed by the Earths umbra. I only ran it on the first 370ish plates as that is all I have downloaded so far.

I also have no idea what I am doing when it comes to proper astronomy so this could be complete garbage. So if someone with actual astronomy knowledge would like to look over the code, that would be much appreciated.

I believe its looking for objects in a 40 km orbit
Python:
#!/usr/bin/env python3
"""
Earth Shadow Field Intersection Analyzer

This script analyzes CSV files containing observation metadata to determine
if the field of view of each photographic plate intersects with Earth's shadow
cone in space, where satellites could be eclipsed.

Author: Analysis for eclipse region intersection with observation fields
"""

import os
import csv
import pandas as pd
from datetime import datetime, timezone
import numpy as np
from pathlib import Path
import json

class EarthShadowFieldAnalyzer:
    def __init__(self, csv_directory="poss_1/red/"):
        self.csv_directory = Path(csv_directory)
        self.earth_radius = 6371  # km
        self.au = 149597870.7  # km (astronomical unit)
        self.results = []

    def parse_observation_time(self, date_obs_str):
        """Parse DATE-OBS string into datetime object"""
        try:
            # Handle format: 1955-04-14T06:32:00
            return datetime.fromisoformat(date_obs_str.replace('Z', '+00:00'))
        except ValueError:
            try:
                # Try alternative formats
                return datetime.strptime(date_obs_str, "%Y-%m-%dT%H:%M:%S")
            except ValueError:
                print(f"Warning: Could not parse date string: {date_obs_str}")
                return None

    def calculate_solar_position(self, dt):
        """Calculate solar position in equatorial coordinates"""
        if dt is None:
            return None, None

        # Julian day calculation
        a = (14 - dt.month) // 12
        y = dt.year - a
        m = dt.month + 12 * a - 3
        jd = dt.day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 + 1721119

        # Add time of day
        jd += (dt.hour + dt.minute/60 + dt.second/3600) / 24

        # Calculate solar coordinates (simplified)
        n = jd - 2451545.0  # Days since J2000
        L = (280.460 + 0.9856474 * n) % 360  # Solar longitude
        g = np.radians((357.528 + 0.9856003 * n) % 360)  # Solar anomaly

        # Solar longitude with equation of center
        lambda_sun = np.radians(L + 1.915 * np.sin(g) + 0.020 * np.sin(2*g))

        # Obliquity of ecliptic
        epsilon = np.radians(23.439 - 0.0000004 * n)

        # Convert to equatorial coordinates
        solar_ra = np.degrees(np.arctan2(np.cos(epsilon) * np.sin(lambda_sun), np.cos(lambda_sun)))
        solar_dec = np.degrees(np.arcsin(np.sin(epsilon) * np.sin(lambda_sun)))

        # Ensure RA is in 0-360 range
        solar_ra = solar_ra % 360

        return solar_ra, solar_dec

    def calculate_antisolar_point(self, solar_ra, solar_dec):
        """Calculate the antisolar point (opposite direction from Sun)"""
        antisolar_ra = (solar_ra + 180) % 360
        antisolar_dec = -solar_dec
        return antisolar_ra, antisolar_dec

    def angular_separation(self, ra1, dec1, ra2, dec2):
        """Calculate angular separation between two points in degrees"""
        ra1, dec1, ra2, dec2 = map(np.radians, [ra1, dec1, ra2, dec2])

        cos_sep = (np.sin(dec1) * np.sin(dec2) +
                   np.cos(dec1) * np.cos(dec2) * np.cos(ra1 - ra2))

        # Handle floating point errors
        cos_sep = np.clip(cos_sep, -1, 1)

        return np.degrees(np.arccos(cos_sep))

    def calculate_shadow_intersection(self, obs_datetime, field_ra, field_dec,
                                    field_size_deg=6, altitude_km=40):
        """
        Calculate if the observation field intersects Earth's shadow cone

        Parameters:
        - obs_datetime: observation time
        - field_ra, field_dec: field center coordinates (degrees)
        - field_size_deg: approximate field of view diameter (degrees)
        - altitude_km: satellite altitude for shadow calculation

        Returns:
        - dict with shadow intersection analysis
        """
        if obs_datetime is None:
            return {"error": "Invalid datetime"}

        # Calculate solar position
        solar_ra, solar_dec = self.calculate_solar_position(obs_datetime)
        if solar_ra is None:
            return {"error": "Could not calculate solar position"}

        # Calculate antisolar point (center of Earth's shadow direction)
        antisolar_ra, antisolar_dec = self.calculate_antisolar_point(solar_ra, solar_dec)

        # Angular separation between field center and antisolar point
        field_to_antisolar = self.angular_separation(field_ra, field_dec,
                                                   antisolar_ra, antisolar_dec)

        # Earth's shadow geometry at satellite altitude
        orbit_radius = self.earth_radius + altitude_km

        # Shadow cone angle calculation
        # Earth's umbra cone half-angle at satellite altitude
        shadow_half_angle = np.degrees(np.arctan(self.earth_radius / orbit_radius))

        # Penumbra considerations (larger shadow region)
        sun_angular_radius = 0.267  # degrees (angular size of Sun from Earth)
        penumbra_half_angle = shadow_half_angle + sun_angular_radius

        # Field of view considerations
        field_radius = field_size_deg / 2

        # Intersection analysis
        # Distance from field center to shadow center
        shadow_center_distance = field_to_antisolar

        # Focus only on umbra intersection
        umbra_intersects = shadow_center_distance < (shadow_half_angle + field_radius)
        field_entirely_in_umbra = shadow_center_distance + field_radius < shadow_half_angle

        # Calculate precise umbra area coverage using circle intersection geometry
        if field_entirely_in_umbra:
            # Entire field is within umbra
            umbra_area_fraction = 1.0
        elif umbra_intersects:
            # Partial intersection - calculate overlapping area of two circles
            # Circle 1: field of view (radius = field_radius)
            # Circle 2: umbra projection (radius = shadow_half_angle)
            # Distance between centers: shadow_center_distance

            r1 = field_radius  # field radius
            r2 = shadow_half_angle  # umbra radius
            d = shadow_center_distance  # distance between centers

            if d >= r1 + r2:
                # No intersection
                umbra_area_fraction = 0.0
            elif d <= abs(r2 - r1):
                # One circle entirely within the other
                if r1 <= r2:
                    # Field entirely within umbra
                    umbra_area_fraction = 1.0
                else:
                    # Umbra entirely within field
                    umbra_area_fraction = (r2 / r1) ** 2
            else:
                # Partial intersection - use circle intersection formula
                # Area of intersection of two circles
                alpha1 = 2 * np.arccos((d**2 + r1**2 - r2**2) / (2 * d * r1))
                alpha2 = 2 * np.arccos((d**2 + r2**2 - r1**2) / (2 * d * r2))

                area1 = 0.5 * r1**2 * (alpha1 - np.sin(alpha1))  # segment of circle 1
                area2 = 0.5 * r2**2 * (alpha2 - np.sin(alpha2))  # segment of circle 2

                intersection_area = area1 + area2
                field_area = np.pi * r1**2

                umbra_area_fraction = intersection_area / field_area

                # Ensure fraction is within bounds
                umbra_area_fraction = np.clip(umbra_area_fraction, 0.0, 1.0)
        else:
            # No intersection
            umbra_area_fraction = 0.0

        return {
            "observation_datetime": obs_datetime.isoformat(),
            "field_center_ra": field_ra,
            "field_center_dec": field_dec,
            "solar_ra": round(solar_ra, 3),
            "solar_dec": round(solar_dec, 3),
            "antisolar_ra": round(antisolar_ra, 3),
            "antisolar_dec": round(antisolar_dec, 3),
            "field_to_antisolar_deg": round(field_to_antisolar, 3),
            "umbra_half_angle_deg": round(shadow_half_angle, 3),
            "field_radius_deg": round(field_radius, 3),
            "umbra_intersects": umbra_intersects,
            "field_entirely_in_umbra": field_entirely_in_umbra,
            "umbra_area_fraction": round(umbra_area_fraction, 4),
            "umbra_area_percentage": round(umbra_area_fraction * 100, 2),
            "satellites_could_be_eclipsed": umbra_intersects,
            "assumed_altitude_km": altitude_km
        }

    def process_csv_file(self, csv_path):
        """Process a single CSV file and extract observation parameters"""
        try:
            # Read CSV file
            df = pd.read_csv(csv_path)

            # Extract key parameters
            params = {}
            for _, row in df.iterrows():
                params[row['Key']] = row['Value']

            # Get required parameters
            date_obs = params.get('DATE-OBS')
            plate_ra = float(params.get('PLATERA', 0))
            plate_dec = float(params.get('PLATEDEC', 0))
            region = params.get('REGION', 'Unknown')
            plate_id = params.get('PLATEID', 'Unknown')

            # Calculate field size from plate dimensions and scale
            plate_scale = float(params.get('PLTSCALE', 67.2))  # arcsec/mm
            plate_size_x = float(params.get('PLTSIZEX', 355))  # mm
            plate_size_y = float(params.get('PLTSIZEY', 355))  # mm

            # Field size in degrees (use diagonal)
            field_size_arcsec = plate_scale * np.sqrt(plate_size_x**2 + plate_size_y**2)
            field_size_deg = field_size_arcsec / 3600

            # Parse observation time
            obs_datetime = self.parse_observation_time(date_obs)

            # Calculate shadow intersection
            shadow_analysis = self.calculate_shadow_intersection(
                obs_datetime, plate_ra, plate_dec, field_size_deg
            )

            # Combine all information
            result = {
                "csv_file": str(csv_path),
                "region": region,
                "plate_id": plate_id,
                "date_obs": date_obs,
                "calculated_field_size_deg": round(field_size_deg, 3),
                **shadow_analysis
            }

            return result

        except Exception as e:
            print(f"Error processing {csv_path}: {e}")
            return {
                "csv_file": str(csv_path),
                "error": str(e)
            }

    def process_all_csv_files(self):
        """Process all CSV files in the directory"""
        if not self.csv_directory.exists():
            print(f"Directory {self.csv_directory} does not exist!")
            return

        csv_files = list(self.csv_directory.glob("*.csv"))
        print(f"Found {len(csv_files)} CSV files to process...")

        for csv_file in csv_files:
            print(f"Processing: {csv_file.name}")
            result = self.process_csv_file(csv_file)
            self.results.append(result)

            # Calculate and display running average (only for valid results)
            valid_results_so_far = [r for r in self.results if 'error' not in r]
            if valid_results_so_far:
                current_coverages = [r.get('umbra_area_fraction', 0) for r in valid_results_so_far]
                running_avg = np.mean(current_coverages)
                print(f"  → Running average umbra coverage: {running_avg:.4f} ({running_avg*100:.2f}%)")

    def save_results(self, output_file="shadow_intersection_analysis.json"):
        """Save results to JSON file"""
        with open(output_file, 'w') as f:
            json.dump(self.results, f, indent=2, default=str)
        print(f"Results saved to {output_file}")

    def generate_summary_report(self):
        """Generate a summary report of the shadow intersection analysis"""
        if not self.results:
            print("No results to summarize!")
            return

        total_observations = len(self.results)
        valid_analyses = [r for r in self.results if 'error' not in r]

        if not valid_analyses:
            print("No valid analyses found!")
            return

        umbra_intersections = sum(1 for r in valid_analyses if r.get('umbra_intersects', False))
        satellites_eclipsed_possible = sum(1 for r in valid_analyses if r.get('satellites_could_be_eclipsed', False))
        field_entirely_in_umbra = sum(1 for r in valid_analyses if r.get('field_entirely_in_umbra', False))
        significant_coverage = sum(1 for r in valid_analyses if r.get('umbra_area_fraction', 0) > 0.1)

        # Calculate average coverage for fields that have any intersection
        intersecting_fields = [r for r in valid_analyses if r.get('umbra_intersects', False)]
        avg_coverage_intersecting = np.mean([r.get('umbra_area_fraction', 0) for r in intersecting_fields]) if intersecting_fields else 0

        # Calculate overall average coverage across ALL plates (including zero coverage)
        all_coverages = [r.get('umbra_area_fraction', 0) for r in valid_analyses]
        overall_avg_coverage = np.mean(all_coverages) if all_coverages else 0

        print("\n" + "="*70)
        print("EARTH UMBRA FIELD INTERSECTION ANALYSIS")
        print("="*70)
        print(f"Total CSV files processed: {total_observations}")
        print(f"Valid analyses: {len(valid_analyses)}")
        print(f"Fields intersecting umbra: {umbra_intersections}")
        print(f"Fields where satellites could be eclipsed: {satellites_eclipsed_possible}")
        print(f"Fields entirely within umbra: {field_entirely_in_umbra}")
        print(f"Fields with >10% umbra coverage: {significant_coverage}")

        print(f"\nPercentages:")
        print(f"  {(satellites_eclipsed_possible/len(valid_analyses)*100):.1f}% of fields could contain eclipsed satellites")
        print(f"  {(umbra_intersections/len(valid_analyses)*100):.1f}% intersect Earth's umbra")
        print(f"  {(field_entirely_in_umbra/len(valid_analyses)*100):.1f}% are entirely within umbra")

        print(f"\nUMBRA COVERAGE STATISTICS:")
        print(f"  Overall average coverage (all plates): {overall_avg_coverage:.1%} ({overall_avg_coverage*100:.2f}%)")
        if intersecting_fields:
            print(f"  Average coverage (intersecting plates only): {avg_coverage_intersecting:.1%}")
            print(f"  Maximum coverage: {max(r.get('umbra_area_fraction', 0) for r in intersecting_fields):.1%}")
            print(f"  Total plates with umbra intersection: {len(intersecting_fields)}")

        # Show best examples
        print(f"\nFields with highest umbra coverage:")
        shadow_fields = [r for r in valid_analyses if r.get('satellites_could_be_eclipsed', False)]
        shadow_fields.sort(key=lambda x: x.get('umbra_area_fraction', 0), reverse=True)

        for example in shadow_fields[:5]:
            print(f"  {example['plate_id']} ({example['region']}) - {example['date_obs']}")
            print(f"    Umbra coverage: {example['umbra_area_percentage']:.2f}% of plate area")
            print(f"    Distance to antisolar point: {example['field_to_antisolar_deg']:.1f}°")

    def create_summary_csv(self, output_file="shadow_intersection_summary.csv"):
        """Create a CSV summary of all analyses"""
        if not self.results:
            print("No results to export!")
            return

        # Filter out error results and create DataFrame
        valid_results = [r for r in self.results if 'error' not in r]
        if valid_results:
            df = pd.DataFrame(valid_results)
            df.to_csv(output_file, index=False)
            print(f"Summary CSV saved to {output_file}")

def main():
    # Initialize analyzer
    analyzer = EarthShadowFieldAnalyzer("poss_1/red/")

    # Process all CSV files
    analyzer.process_all_csv_files()

    # Save detailed results
    analyzer.save_results("shadow_intersection_analysis.json")

    # Create summary CSV
    analyzer.create_summary_csv("shadow_intersection_summary.csv")

    # Generate summary report
    analyzer.generate_summary_report()

    print("\nAnalysis complete!")
    print("Files created:")
    print("  - shadow_intersection_analysis.json (detailed results)")
    print("  - shadow_intersection_summary.csv (summary table)")
    print("\nLook for fields with 'satellites_could_be_eclipsed' = True")
    print("Focus on 'umbra_area_percentage' for coverage of plate area")

if __name__ == "__main__":
    main()
======================================================================
EARTH UMBRA FIELD INTERSECTION ANALYSIS
======================================================================
Total CSV files processed: 373
Valid analyses: 373
Fields intersecting umbra: 115
Fields where satellites could be eclipsed: 115
Fields entirely within umbra: 67
Fields with >10% umbra coverage: 111

Percentages:
30.8% of fields could contain eclipsed satellites
30.8% intersect Earth's umbra
18.0% are entirely within umbra

UMBRA COVERAGE STATISTICS:
Overall average coverage (all plates): 25.7% (25.70%)
Average coverage (intersecting plates only): 83.4%
Maximum coverage: 100.0%
Total plates with umbra intersection: 115

Fields with highest umbra coverage:
06SD (XE301) - 1955-11-10T08:57:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 19.4°
082Q (XE213) - 1953-03-08T08:07:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 37.4°
06O3 (XE309) - 1953-02-15T05:47:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 37.6°
06S1 (XE294) - 1952-09-16T07:52:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 38.6°
0CRH (XE292) - 1954-10-30T04:18:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 34.4°

Analysis complete!
Files created:
- shadow_intersection_analysis.json (detailed results)
- shadow_intersection_summary.csv (summary table)

Look for fields with 'satellites_could_be_eclipsed' = True
Focus on 'umbra_area_percentage' for coverage of plate area
 

Attachments

Claude's script is trying to get fields that don't exist and then using default values that are probably wrong when getting them fails.

Here is an example fits header. I think you need to figure out how to use the metadata to correctly to get the plate center and size, etc. Also, asking to use astropy might reduce the chance it produces errors, and that it will be easier to verify because it will be much shorter and simpler code, with less of claude reinventing the wheel. So far I find AI is not very good at astronomy data processing.

Code:
IMPLE  =                    T /FITS header
BITPIX  =                   16 /No.Bits per pixel
NAXIS   =                    2 /No.dimensions
NAXIS1  =                  177 /Length X axis
NAXIS2  =                  177 /Length Y axis
EXTEND  =                    T /
DATE    = '14/08/25          ' /Date of FITS file creation
ORIGIN  = 'CASB -- STScI     ' /Origin of FITS image
PLTLABEL= 'E1611             ' /Observatory plate label
PLATEID = '092U              ' /GSSS Plate ID
REGION  = 'XE554             ' /GSSS Region Name
DATE-OBS= '28/04/57          ' /UT date of Observation
UT      = '05:34:00.00       ' /UT time of observation
EPOCH   =  1.9573211669922E+03 /Epoch of plate
PLTRAH  =                   12 /Plate center RA
PLTRAM  =                    7 /
PLTRAS  =  2.5341930000000E+01 /
PLTDECSN= '+                 ' /Plate center Dec
PLTDECD =                    5 /
PLTDECM =                   11 /
PLTDECS =  3.4089730000000E+01 /
EQUINOX =  2.0000000000000E+03 /Julian Reference frame equinox
EXPOSURE=  5.0000000000000E+01 /Exposure time minutes
BANDPASS=                    8 /GSSS Bandpass code
PLTGRADE=                    0 /Plate grade
PLTSCALE=  6.7200000000000E+01 /Plate Scale arcsec per mm
SITELAT = '+33:24:24.00      ' /Latitude of Observatory
SITELONG= '-116:51:48.00     ' /Longitude of Observatory
TELESCOP= 'Palomar 48-inch Schmidt'/Telescope where plate taken
CNPIX1  =                10122 /X corner  (pixels)
CNPIX2  =                 3055 /Y corner
DATATYPE= 'INTEGER*2         ' /Type of Data
SCANIMG = 'XE554_092U_00_03.PIM'/Name of original scan
SCANNUM =                    0 /Identifies scan of the plate
DCHOPPED=                    T /Image repaired for chopping effects
DSHEARED=                    F /Image repaired for shearing effects
DSCNDNUM=                    3 /Identifies descendant of plate scan image
XPIXELSZ=  2.5284450000000E+01 /X pixel size microns
YPIXELSZ=  2.5284450000000E+01 /Y pixel size microns
PPO1    =  0.0000000000000E+00 /Orientation Coefficients
PPO2    =  0.0000000000000E+00 /
PPO3    =  1.7747471555000E+05 /
PPO4    =  0.0000000000000E+00 /
PPO5    =  0.0000000000000E+00 /
PPO6    =  1.7747471555000E+05 /
AMDX1   =  6.7241400000000E+01 /Plate solution x coefficients
AMDX2   = -8.8491000000000E-02 /
AMDX3   = -4.6638720000000E+02 /
AMDX4   = -4.0451650000000E-05 /
AMDX5   = -1.6714420000000E-05 /
AMDX6   =  0.0000000000000E+00 /
AMDX7   =  0.0000000000000E+00 /
AMDX8   =  0.0000000000000E+00 /
AMDX9   =  0.0000000000000E+00 /
AMDX10  =  0.0000000000000E+00 /
AMDX11  =  0.0000000000000E+00 /
AMDX12  =  2.0713620000000E-06 /
AMDX13  =  0.0000000000000E+00 /
AMDX14  =  0.0000000000000E+00 /
AMDX15  =  0.0000000000000E+00 /
AMDX16  =  0.0000000000000E+00 /
AMDX17  =  0.0000000000000E+00 /
AMDX18  =  0.0000000000000E+00 /
AMDX19  =  0.0000000000000E+00 /
AMDX20  =  0.0000000000000E+00 /
AMDY1   =  6.7262660000000E+01 /Plate solution y coefficients
AMDY2   =  8.6033300000000E-02 /
AMDY3   = -2.0269220000000E+01 /
AMDY4   = -1.1512380000000E-05 /
AMDY5   = -4.2587770000000E-05 /
AMDY6   =  0.0000000000000E+00 /
AMDY7   =  0.0000000000000E+00 /
AMDY8   =  0.0000000000000E+00 /
AMDY9   =  0.0000000000000E+00 /
AMDY10  =  0.0000000000000E+00 /
AMDY11  =  0.0000000000000E+00 /
AMDY12  =  1.8867340000000E-06 /
AMDY13  =  0.0000000000000E+00 /
AMDY14  =  0.0000000000000E+00 /
AMDY15  =  0.0000000000000E+00 /
AMDY16  =  0.0000000000000E+00 /
AMDY17  =  0.0000000000000E+00 /
AMDY18  =  0.0000000000000E+00 /
AMDY19  =  0.0000000000000E+00 /
AMDY20  =  0.0000000000000E+00 /
        Based on photographic data of the National Geographic Society -- Palomar
        Observatory Sky Survey (NGS-POSS) obtained using the Oschin Telescope on
        Palomar Mountain.  The NGS-POSS was funded by a grant from the National
        Geographic Society to the California Institute of Technology.  The
        plates were processed into the present compressed digital form with
        their permission.  The Digitized Sky Survey was produced at the Space
        Telescope Science Institute under US Government grant NAG W-2166.

        Investigators using these scans are requested to include the above
        acknowledgements in any publications.

        Copyright (c) 1994, Association of Universities for Research in
        Astronomy, Inc.  All rights reserved.
DATAMAX =                14570 /Maximum data value
DATAMIN =                 2455 /Minimum data value
OBJECT  = 'dss10452          ' /Object ID
OBJCTRA = '12 00 52.541      ' /Object Right Ascension (J2000)
OBJCTDEC= '+03 21 09.00      ' /Object Declination (J2000)
OBJCTX  =             10210.44 /Object X on plate (pixels)
OBJCTY  =              3143.18 /Object Y on plate (pixels)
CTYPE1  = 'RA---TAN'           /R.A. in tangent plane projection
CTYPE2  = 'DEC--TAN'           /DEC. in tangent plane projection
CRPIX1  =                 88.5 /Refpix of first axis
CRPIX2  =                 88.5 /Refpix of second axis
CRVAL1  =  1.8021912882297E+02 /RA at Ref pix in decimal degrees
CRVAL2  =  3.3524139989080E+00 /DEC at Ref pix in decimal degrees
CROTA1  =  1.9623839649273E-01 /Rotation angle of first axis (deg)
CROTA2  =  1.9623839649273E-01 /Rotation angle of second axis (deg)
        Warning: CROTA2 is inaccurate due to considerable skew
SKEW    =  2.5329910096062E-01,  1.3917769202484E-01 /Measure of skew
CDELT1  = -4.7260612906910E-04 /RA pixel step (deg)
CDELT2  =  4.7273143283020E-04 /DEC pixel step (deg)
CD1_1   = -4.7260473474692E-04 /CD matrix
CD1_2   = -2.0898931457244E-06 /CD matrix
CD2_1   = -1.1480106589599E-06 /CD matrix
CD2_2   =  4.7272681321492E-04 /CD matrix
PC001001=  9.9999704971625E-01 /PC matrix
PC001002=  4.4220610296378E-03 /PC matrix
PC002001= -2.4284627152608E-03 /PC matrix
PC002002=  9.9999022782290E-01 /PC matrix
END


Note plate center coords are in sexigesimal, and you need to get and assemble the 3 values per RA, DEC, ...

Also, it is only using the date and not time, and I think parsing it incorrectly. Here is the correct date time extraction.

Python:
# converts to iso format
def time_str( date_obs, ut ) :
    date_split = date_obs.split( "/" )
    return f"19{date_split[2]}-{date_split[1]}-{date_split[0]} {ut}"

t = Time( time_str( header["DATE-OBS"], header["UT"] ), scale="utc", format="iso" )

I assume Claude Sonnet made many more errors, but these are some of the most obvious without analyzing its algorithms.
 
Last edited:
I only saved a small subset of the fits file to a csv.

I didn't save what didn't seem immediately useful since I had to define keys manually.


trying to get fields that don't exist and then using default values
Which values are the problematic ones?


Also, asking to use astropy might reduce the chance it produces errors
Will do
 

Attachments

I only saved a small subset of the fits file to a csv.

I didn't save what didn't seem immediately useful since I had to define keys manually.



Which values are the problematic ones?



Will do
I found where I think it was falling back to default values if they are missing.

Python:
# Get required parameters
            date_obs = params.get('DATE-OBS')
            plate_ra = float(params.get('PLATERA'))
            plate_dec = float(params.get('PLATEDEC'))
            region = params.get('REGION')
            plate_id = params.get('PLATEID')

            # Calculate field size from plate dimensions and scale
            plate_scale = float(params.get('PLTSCALE'))  # arcsec/mm
            plate_size_x = float(params.get('PLTSIZEX'))  # mm
            plate_size_y = float(params.get('PLTSIZEY'))  # mm

With that modification, the output is still the same
 
I only saved a small subset of the fits file to a csv.

I didn't save what didn't seem immediately useful since I had to define keys manually.



Which values are the problematic ones?



Will do

There is no PLATERA or PLATEDEC (claude assumed those exist and are in decimal), but they are actually these that you need to get and assemble (3 values for RA in sexigesimal and same with DEC).
PLTRAH = 12 /Plate center RA
PLTRAM = 7 /
PLTRAS = 2.5341930000000E+01 /
PLTDECSN= '+ ' /Plate center Dec
PLTDECD = 5 /
PLTDECM = 11 /
PLTDECS = 3.4089730000000E+01 /

And it needs to use UT from the header for the time, instead of 00:00:00.

When it does this, it is defaulting to 67.2 if the key isn't in params. You should remove all of the defaults, and let it throw an error instead to detect when it is trying to use parameters that don't exist.

1755388093056.png



PLTSCALE exists, but PLTSIZEX and PLTSIZEY don't.
 
Ahh, I thought your data was the same as my fit files? I must have different data, this is the whole plate data you downloaded?
 
Last edited:
There is no PLATERA or PLATEDEC (claude assumed those exist and are in decimal), but they are actually these that you need to get and assemble (3 values for RA in sexigesimal and same with DEC).
My original fit files did contain these values apparently.

I'm hoping I pulled all the relevant ones lol.

Original Fit file for XE183
Code:
SIMPLE  =                    T /FITS: Compliance
BITPIX  =                   16 /FITS: I*2 Data
NAXIS   =                    2 /FITS: 2-D Image Data
NAXIS1  =                14000 /FITS: X Dimension
NAXIS2  =                13999 /FITS: Y Dimension
DATE    = '2006-04-04T16:02:22' /FITS: Creation Date
ORIGIN  = 'STScI/MAST'         /GSSS: STScI Digitized Sky Survey
SURVEY  = 'POSSI-E '           /GSSS: Sky Survey
REGION  = 'XE183   '           /GSSS: Region Name
PLATEID = '07W1    '           /GSSS: Plate ID
SCANNUM = '00      '           /GSSS: Scan Number
DSCNDNUM= '00      '           /GSSS: Descendant Number
TELESCID=                    1 /GSSS: Telescope ID
BANDPASS=                    8 /GSSS: Bandpass Code
COPYRGHT= 'STScI/AURA'         /GSSS: Copyright Holder
SITELAT =               33.356 /Observatory: Latitude
SITELONG=              116.863 /Observatory: Longitude
TELESCOP= 'Palomar Schmidt   ' /Observatory: Telescope
INSTRUME= 'Photographic Plate' /Detector: Photographic Plate
EMULSION= '103aE   '           /Detector: Emulsion
FILTER  = 'plexi   '           /Detector: Filter
PLTSCALE=                67.20 /Detector: Plate Scale arcsec per mm
PLTSIZEX=              355.000 /Detector: Plate X Dimension mm
PLTSIZEY=              355.000 /Detector: Plate Y Dimension mm
PLATERA =        290.007823595 /Observation: Field centre RA degrees
PLATEDEC=        48.2695418668 /Observation: Field centre Dec degrees
PLTLABEL= 'E814    '           /Observation: Plate Label
DATE-OBS= '1953-09-08T03:57:00' /Observation: Date/Time
EXPOSURE=                 45.0 /Observation: Exposure Minutes
PLTGRADE= '1       '           /Observation: Plate Grade
OBSHA   =             0.416667 /Observation: Hour Angle
OBSZD   =              15.6051 /Observation: Zenith Distance
AIRMASS =              1.03819 /Observation: Airmass
REFBETA =        61.8196598237 /Observation: Refraction Coeff
REFBETAP=     -0.0820000000000 /Observation: Refraction Coeff
REFK1   =       -1946.43270999 /Observation: Refraction Coeff
REFK2   =       0.000000000000 /Observation: Refraction Coeff
CNPIX1  =                    0 /Scan: X Corner
CNPIX2  =                    0 /Scan: Y Corner
XPIXELS =                14000 /Scan: X Dimension
YPIXELS =                13999 /Scan: Y Dimension
XPIXELSZ=              25.2845 /Scan: Pixel Size microns
YPIXELSZ=              25.2845 /Scan: Pixel Size microns
PPO1    =       0.000000000000 /Scan: Orientation Coeff
PPO2    =       0.000000000000 /Scan: Orientation Coeff
PPO3    =        177500.000000 /Scan: Orientation Coeff
PPO4    =       0.000000000000 /Scan: Orientation Coeff
PPO5    =       0.000000000000 /Scan: Orientation Coeff
PPO6    =        177500.000000 /Scan: Orientation Coeff
PLTRAH  =                   19 /Astrometry: Plate Centre H
PLTRAM  =                   20 /Astrometry: Plate Centre M
PLTRAS  =                 1.88 /Astrometry: Plate Centre S
PLTDECSN= '+       '           /Astrometry: Plate Centre +/-
PLTDECD =                   48 /Astrometry: Plate Centre D
PLTDECM =                   16 /Astrometry: Plate Centre M
PLTDECS =                 10.4 /Astrometry: Plate Centre S
EQUINOX =               2000.0 /Astrometry: Equinox
AMDX1   =        67.2478071887 /Astrometry: GSC1 Coeff
AMDX2   =      0.0429814229897 /Astrometry: GSC1 Coeff
AMDX3   =       -370.143278733 /Astrometry: GSC1 Coeff
AMDX4   =  -1.00794215255E-005 /Astrometry: GSC1 Coeff
AMDX5   =   1.38046667611E-005 /Astrometry: GSC1 Coeff
AMDX6   =   1.45861190363E-006 /Astrometry: GSC1 Coeff
AMDX7   =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX8   =   1.76104124797E-006 /Astrometry: GSC1 Coeff
AMDX9   =  -9.81128000611E-008 /Astrometry: GSC1 Coeff
AMDX10  =   2.23112710870E-006 /Astrometry: GSC1 Coeff
AMDX11  =  -2.66926251839E-008 /Astrometry: GSC1 Coeff
AMDX12  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX13  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX14  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX15  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX16  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX17  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX18  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX19  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDX20  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY1   =        67.2652748711 /Astrometry: GSC1 Coeff
AMDY2   =     -0.0451400668325 /Astrometry: GSC1 Coeff
AMDY3   =        89.8601124787 /Astrometry: GSC1 Coeff
AMDY4   =   7.84631444423E-006 /Astrometry: GSC1 Coeff
AMDY5   =  -4.34913966805E-005 /Astrometry: GSC1 Coeff
AMDY6   =   3.87417681630E-007 /Astrometry: GSC1 Coeff
AMDY7   =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY8   =   1.74441605744E-006 /Astrometry: GSC1 Coeff
AMDY9   =  -1.71837494325E-008 /Astrometry: GSC1 Coeff
AMDY10  =   2.32496386735E-006 /Astrometry: GSC1 Coeff
AMDY11  =  -1.97486588518E-008 /Astrometry: GSC1 Coeff
AMDY12  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY13  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY14  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY15  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY16  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY17  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY18  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY19  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDY20  =       0.000000000000 /Astrometry: GSC1 Coeff
AMDREX1 =        67.2440762141 /Astrometry: GSC2 Coeff
AMDREX2 =      0.0418648166846 /Astrometry: GSC2 Coeff
AMDREX3 =       -371.666125309 /Astrometry: GSC2 Coeff
AMDREX4 =   1.96380448784E-005 /Astrometry: GSC2 Coeff
AMDREX5 =   1.03113950874E-006 /Astrometry: GSC2 Coeff
AMDREX6 =   5.44252800248E-006 /Astrometry: GSC2 Coeff
AMDREX7 =       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX8 =  -2.98718623321E-007 /Astrometry: GSC2 Coeff
AMDREX9 =  -1.00745263350E-007 /Astrometry: GSC2 Coeff
AMDREX10=   6.74299665200E-008 /Astrometry: GSC2 Coeff
AMDREX11=   2.33568968780E-008 /Astrometry: GSC2 Coeff
AMDREX12=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX13=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX14=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX15=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX16=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX17=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX18=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX19=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREX20=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY1 =        67.2610715343 /Astrometry: GSC2 Coeff
AMDREY2 =     -0.0460447173855 /Astrometry: GSC2 Coeff
AMDREY3 =        91.4508075275 /Astrometry: GSC2 Coeff
AMDREY4 =  -3.04827094441E-006 /Astrometry: GSC2 Coeff
AMDREY5 =  -7.10257472653E-006 /Astrometry: GSC2 Coeff
AMDREY6 =  -2.26385499115E-006 /Astrometry: GSC2 Coeff
AMDREY7 =       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY8 =  -2.65716365821E-007 /Astrometry: GSC2 Coeff
AMDREY9 =  -2.30628322305E-008 /Astrometry: GSC2 Coeff
AMDREY10=   1.26630603913E-008 /Astrometry: GSC2 Coeff
AMDREY11=   3.76872239372E-008 /Astrometry: GSC2 Coeff
AMDREY12=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY13=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY14=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY15=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY16=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY17=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY18=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY19=       0.000000000000 /Astrometry: GSC2 Coeff
AMDREY20=       0.000000000000 /Astrometry: GSC2 Coeff
ASTRMASK= 'xe.mask '           /Astrometry: GSC2 Mask
END

When it does this, it is defaulting to 67.2 if the key isn't in params. You should remove all of the defaults, and let it throw an error instead to detect when it is trying to use parameters that don't exist.
I missed that entirely when I first looked at it.
I didn't even realize you could do that in python. That would have saved me alot of messy code.
 
Ahh, I thought your data was the same as my fit files? I must have different data, this is the whole plate data you downloaded?
I only pulled a subset into my csv files. I was too lazy to add all the keys originally.
Edit: This the the header data from the whole plate fit file. I forgot to mention that.

Here is what I reduced it too
Key,Value,Comment
DATE-OBS,1951-02-03T07:17:00,Observation: Date/Time
REGION,XE369,GSSS: Region Name
PLATEID,0728,GSSS: Plate ID
PLATERA,138.596038795,Observation: Field centre RA degrees
PLATEDEC,23.4043521418,Observation: Field centre Dec degrees
PLTSCALE,67.20,Detector: Plate Scale arcsec per mm
PLTSIZEX,355.000,Detector: Plate X Dimension mm
PLTSIZEY,355.000,Detector: Plate Y Dimension mm
NAXIS1,14000,FITS: X Dimension
NAXIS2,13999,FITS: Y Dimension

I have instructed claude to use Astropy instead.

======================================================================
EARTH UMBRA FIELD INTERSECTION ANALYSIS (using Astropy)
======================================================================
Total CSV files processed: 394
Valid analyses: 394
Fields intersecting umbra: 121
Fields where satellites could be eclipsed: 121
Fields entirely within umbra: 60
Fields with >10% umbra coverage: 112

Percentages:
30.7% of fields could contain eclipsed satellites
30.7% intersect Earth's umbra
15.2% are entirely within umbra

UMBRA COVERAGE STATISTICS:
Overall average coverage (all plates): 23.5% (23.49%)
Average coverage (intersecting plates only): 76.5%
Maximum coverage: 100.0%
Total plates with umbra intersection: 121

Fields with highest umbra coverage:
06SD (XE301) - 1955-11-10T08:57:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 19.3°
082Q (XE213) - 1953-03-08T08:07:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 37.5°
072H (XE380) - 1950-04-11T08:43:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 33.1°
06O3 (XE309) - 1953-02-15T05:47:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 37.8°
06S1 (XE294) - 1952-09-16T07:52:00
Umbra coverage: 100.00% of plate area
Distance to antisolar point: 38.5°

Analysis complete!
Files created:
- shadow_intersection_analysis.json (detailed results)
- shadow_intersection_summary.csv (summary table)

Look for fields with 'satellites_could_be_eclipsed' = True
Focus on 'umbra_area_percentage' for coverage of plate area

Actual Code

Python:
#!/usr/bin/env python3
"""
Earth Shadow Field Intersection Analyzer (using Astropy)

This script analyzes CSV files containing observation metadata to determine
if the field of view of each photographic plate intersects with Earth's shadow
cone in space, where satellites could be eclipsed.

Uses Astropy for simplified astronomical calculations.

Author: Analysis for eclipse region intersection with observation fields
"""

import os
import csv
import pandas as pd
import numpy as np
from pathlib import Path
import json
import warnings

# Astropy imports
from astropy.time import Time
from astropy.coordinates import SkyCoord, get_sun
from astropy import units as u
from astropy.utils.exceptions import AstropyWarning

# Suppress ERFA warnings for historical dates
warnings.filterwarnings('ignore', category=AstropyWarning, module='astropy')
warnings.filterwarnings('ignore', message='.*ERFA function.*dubious year.*')

class EarthShadowFieldAnalyzer:
    def __init__(self, csv_directory="poss_1/red/"):
        self.csv_directory = Path(csv_directory)
        self.earth_radius = 6371  # km
        self.results = []

    def parse_observation_time(self, date_obs_str):
        """Parse DATE-OBS string into Astropy Time object"""
        try:
            # Handle format: 1955-04-14T06:32:00
            return Time(date_obs_str, format='isot', scale='utc')
        except ValueError:
            try:
                # Try without explicit format
                return Time(date_obs_str, scale='utc')
            except ValueError:
                print(f"Warning: Could not parse date string: {date_obs_str}")
                return None

    def calculate_antisolar_point(self, obs_time):
        """Calculate the antisolar point using Astropy"""
        if obs_time is None:
            return None

        # Get solar position
        sun = get_sun(obs_time)

        # Antisolar point is 180 degrees away in RA, opposite declination
        antisolar_ra = (sun.ra + 180 * u.deg).wrap_at(360 * u.deg)
        antisolar_dec = -sun.dec

        antisolar_coord = SkyCoord(ra=antisolar_ra, dec=antisolar_dec, frame='icrs')

        return sun, antisolar_coord

    def calculate_shadow_intersection(self, obs_time, field_ra, field_dec,
                                    field_size_deg=6, altitude_km=40):
        """
        Calculate if the observation field intersects Earth's shadow cone

        Parameters:
        - obs_time: Astropy Time object
        - field_ra, field_dec: field center coordinates (degrees)
        - field_size_deg: approximate field of view diameter (degrees)
        - altitude_km: satellite altitude for shadow calculation

        Returns:
        - dict with shadow intersection analysis
        """
        if obs_time is None:
            return {"error": "Invalid datetime"}

        # Calculate solar and antisolar positions
        solar_antisolar = self.calculate_antisolar_point(obs_time)
        if solar_antisolar is None:
            return {"error": "Could not calculate solar position"}

        sun_coord, antisolar_coord = solar_antisolar

        # Create field center coordinate
        field_coord = SkyCoord(ra=field_ra * u.deg, dec=field_dec * u.deg, frame='icrs')

        # Angular separation between field center and antisolar point
        field_to_antisolar = field_coord.separation(antisolar_coord).degree

        # Earth's shadow geometry at satellite altitude
        orbit_radius = self.earth_radius + altitude_km

        # Shadow cone angle calculation
        # Earth's umbra cone half-angle at satellite altitude
        shadow_half_angle = np.degrees(np.arctan(self.earth_radius / orbit_radius))

        # Field of view considerations
        field_radius = field_size_deg / 2

        # Focus only on umbra intersection
        umbra_intersects = field_to_antisolar < (shadow_half_angle + field_radius)
        field_entirely_in_umbra = field_to_antisolar + field_radius < shadow_half_angle

        # Calculate precise umbra area coverage using circle intersection geometry
        if field_entirely_in_umbra:
            # Entire field is within umbra
            umbra_area_fraction = 1.0
        elif umbra_intersects:
            # Partial intersection - calculate overlapping area of two circles
            # Circle 1: field of view (radius = field_radius)
            # Circle 2: umbra projection (radius = shadow_half_angle)
            # Distance between centers: field_to_antisolar

            r1 = field_radius  # field radius
            r2 = shadow_half_angle  # umbra radius
            d = field_to_antisolar  # distance between centers

            if d >= r1 + r2:
                # No intersection
                umbra_area_fraction = 0.0
            elif d <= abs(r2 - r1):
                # One circle entirely within the other
                if r1 <= r2:
                    # Field entirely within umbra
                    umbra_area_fraction = 1.0
                else:
                    # Umbra entirely within field
                    umbra_area_fraction = (r2 / r1) ** 2
            else:
                # Partial intersection - use circle intersection formula
                # Area of intersection of two circles
                alpha1 = 2 * np.arccos((d**2 + r1**2 - r2**2) / (2 * d * r1))
                alpha2 = 2 * np.arccos((d**2 + r2**2 - r1**2) / (2 * d * r2))

                area1 = 0.5 * r1**2 * (alpha1 - np.sin(alpha1))  # segment of circle 1
                area2 = 0.5 * r2**2 * (alpha2 - np.sin(alpha2))  # segment of circle 2

                intersection_area = area1 + area2
                field_area = np.pi * r1**2

                umbra_area_fraction = intersection_area / field_area

                # Ensure fraction is within bounds
                umbra_area_fraction = np.clip(umbra_area_fraction, 0.0, 1.0)
        else:
            # No intersection
            umbra_area_fraction = 0.0

        return {
            "observation_datetime": obs_time.iso,
            "field_center_ra": field_ra,
            "field_center_dec": field_dec,
            "solar_ra": round(sun_coord.ra.degree, 3),
            "solar_dec": round(sun_coord.dec.degree, 3),
            "antisolar_ra": round(antisolar_coord.ra.degree, 3),
            "antisolar_dec": round(antisolar_coord.dec.degree, 3),
            "field_to_antisolar_deg": round(field_to_antisolar, 3),
            "umbra_half_angle_deg": round(shadow_half_angle, 3),
            "field_radius_deg": round(field_radius, 3),
            "umbra_intersects": umbra_intersects,
            "field_entirely_in_umbra": field_entirely_in_umbra,
            "umbra_area_fraction": round(umbra_area_fraction, 4),
            "umbra_area_percentage": round(umbra_area_fraction * 100, 2),
            "satellites_could_be_eclipsed": umbra_intersects,
            "assumed_altitude_km": altitude_km
        }

    def process_csv_file(self, csv_path):
        """Process a single CSV file and extract observation parameters"""
        try:
            # Read CSV file
            df = pd.read_csv(csv_path)

            # Extract key parameters
            params = {}
            for _, row in df.iterrows():
                params[row['Key']] = row['Value']

            # Get required parameters (no defaults - fail if missing)
            date_obs = params['DATE-OBS']
            plate_ra = float(params['PLATERA'])
            plate_dec = float(params['PLATEDEC'])
            region = params['REGION']
            plate_id = params['PLATEID']

            # Calculate field size from plate dimensions and scale (no defaults)
            plate_scale = float(params['PLTSCALE'])  # arcsec/mm
            plate_size_x = float(params['PLTSIZEX'])  # mm
            plate_size_y = float(params['PLTSIZEY'])  # mm

            # Field size in degrees (use diagonal)
            field_size_arcsec = plate_scale * np.sqrt(plate_size_x**2 + plate_size_y**2)
            field_size_deg = field_size_arcsec / 3600

            # Parse observation time
            obs_time = self.parse_observation_time(date_obs)

            # Calculate shadow intersection
            shadow_analysis = self.calculate_shadow_intersection(
                obs_time, plate_ra, plate_dec, field_size_deg
            )

            # Combine all information
            result = {
                "csv_file": str(csv_path),
                "region": region,
                "plate_id": plate_id,
                "date_obs": date_obs,
                "calculated_field_size_deg": round(field_size_deg, 3),
                **shadow_analysis
            }

            return result

        except Exception as e:
            print(f"Error processing {csv_path}: {e}")
            return {
                "csv_file": str(csv_path),
                "error": str(e)
            }

    def process_all_csv_files(self):
        """Process all CSV files in the directory"""
        if not self.csv_directory.exists():
            print(f"Directory {self.csv_directory} does not exist!")
            return

        csv_files = list(self.csv_directory.glob("*.csv"))
        print(f"Found {len(csv_files)} CSV files to process...")

        for csv_file in csv_files:
            print(f"Processing: {csv_file.name}")
            result = self.process_csv_file(csv_file)
            self.results.append(result)

            # Calculate and display running average (only for valid results)
            valid_results_so_far = [r for r in self.results if 'error' not in r]
            if valid_results_so_far:
                current_coverages = [r.get('umbra_area_fraction', 0) for r in valid_results_so_far]
                running_avg = np.mean(current_coverages)
                print(f"  → Running average umbra coverage: {running_avg:.4f} ({running_avg*100:.2f}%)")

    def save_results(self, output_file="shadow_intersection_analysis.json"):
        """Save results to JSON file"""
        with open(output_file, 'w') as f:
            json.dump(self.results, f, indent=2, default=str)
        print(f"Results saved to {output_file}")

    def generate_summary_report(self):
        """Generate a summary report of the shadow intersection analysis"""
        if not self.results:
            print("No results to summarize!")
            return

        total_observations = len(self.results)
        valid_analyses = [r for r in self.results if 'error' not in r]

        if not valid_analyses:
            print("No valid analyses found!")
            return

        umbra_intersections = sum(1 for r in valid_analyses if r.get('umbra_intersects', False))
        satellites_eclipsed_possible = sum(1 for r in valid_analyses if r.get('satellites_could_be_eclipsed', False))
        field_entirely_in_umbra = sum(1 for r in valid_analyses if r.get('field_entirely_in_umbra', False))
        significant_coverage = sum(1 for r in valid_analyses if r.get('umbra_area_fraction', 0) > 0.1)

        # Calculate average coverage for fields that have any intersection
        intersecting_fields = [r for r in valid_analyses if r.get('umbra_intersects', False)]
        avg_coverage_intersecting = np.mean([r.get('umbra_area_fraction', 0) for r in intersecting_fields]) if intersecting_fields else 0

        # Calculate overall average coverage across ALL plates (including zero coverage)
        all_coverages = [r.get('umbra_area_fraction', 0) for r in valid_analyses]
        overall_avg_coverage = np.mean(all_coverages) if all_coverages else 0

        print("\n" + "="*70)
        print("EARTH UMBRA FIELD INTERSECTION ANALYSIS (using Astropy)")
        print("="*70)
        print(f"Total CSV files processed: {total_observations}")
        print(f"Valid analyses: {len(valid_analyses)}")
        print(f"Fields intersecting umbra: {umbra_intersections}")
        print(f"Fields where satellites could be eclipsed: {satellites_eclipsed_possible}")
        print(f"Fields entirely within umbra: {field_entirely_in_umbra}")
        print(f"Fields with >10% umbra coverage: {significant_coverage}")

        print(f"\nPercentages:")
        print(f"  {(satellites_eclipsed_possible/len(valid_analyses)*100):.1f}% of fields could contain eclipsed satellites")
        print(f"  {(umbra_intersections/len(valid_analyses)*100):.1f}% intersect Earth's umbra")
        print(f"  {(field_entirely_in_umbra/len(valid_analyses)*100):.1f}% are entirely within umbra")

        print(f"\nUMBRA COVERAGE STATISTICS:")
        print(f"  Overall average coverage (all plates): {overall_avg_coverage:.1%} ({overall_avg_coverage*100:.2f}%)")
        if intersecting_fields:
            print(f"  Average coverage (intersecting plates only): {avg_coverage_intersecting:.1%}")
            print(f"  Maximum coverage: {max(r.get('umbra_area_fraction', 0) for r in intersecting_fields):.1%}")
            print(f"  Total plates with umbra intersection: {len(intersecting_fields)}")

        # Show best examples
        print(f"\nFields with highest umbra coverage:")
        shadow_fields = [r for r in valid_analyses if r.get('satellites_could_be_eclipsed', False)]
        shadow_fields.sort(key=lambda x: x.get('umbra_area_fraction', 0), reverse=True)

        for example in shadow_fields[:5]:
            print(f"  {example['plate_id']} ({example['region']}) - {example['date_obs']}")
            print(f"    Umbra coverage: {example['umbra_area_percentage']:.2f}% of plate area")
            print(f"    Distance to antisolar point: {example['field_to_antisolar_deg']:.1f}°")

    def create_summary_csv(self, output_file="shadow_intersection_summary.csv"):
        """Create a CSV summary of all analyses"""
        if not self.results:
            print("No results to export!")
            return

        # Filter out error results and create DataFrame
        valid_results = [r for r in self.results if 'error' not in r]
        if valid_results:
            df = pd.DataFrame(valid_results)
            df.to_csv(output_file, index=False)
            print(f"Summary CSV saved to {output_file}")

def main():
    # Check if astropy is available
    try:
        import astropy
        print(f"Using Astropy version {astropy.__version__}")
    except ImportError:
        print("Error: Astropy is required but not installed.")
        print("Please install it with: pip install astropy")
        return

    # Initialize analyzer
    analyzer = EarthShadowFieldAnalyzer("poss_1/red/")

    # Process all CSV files
    analyzer.process_all_csv_files()

    # Save detailed results
    analyzer.save_results("shadow_intersection_analysis.json")

    # Create summary CSV
    analyzer.create_summary_csv("shadow_intersection_summary.csv")

    # Generate summary report
    analyzer.generate_summary_report()

    print("\nAnalysis complete!")
    print("Files created:")
    print("  - shadow_intersection_analysis.json (detailed results)")
    print("  - shadow_intersection_summary.csv (summary table)")
    print("\nLook for fields with 'satellites_could_be_eclipsed' = True")
    print("Focus on 'umbra_area_percentage' for coverage of plate area")

if __name__ == "__main__":
    main()
 
One thing I notice is that GEO is 42,164 KM from Earth center. But this code seems to be using 40 KM + Earth radius?

Maybe you can try having claude review https://github.com/guynir42/earthshadow/blob/main/src/earthshadow.py as a reference?

Based on Mick's plot, maybe the high percentages you're getting make sense if you're using 40 KM?

View attachment 83202
Oh.
It originally wanted 40,000 Km but I misread the wiki page and set it to 40km...

Its down to 0.55% coverage for the first 431 plates.

Percentages:
1.6% of fields could contain eclipsed satellites
1.6% intersect Earth's umbra
0.0% are entirely within umbra

UMBRA COVERAGE STATISTICS:
Overall average coverage (all plates): 0.5% (0.55%)
Average coverage (intersecting plates only): 33.6%
Maximum coverage: 89.0%
Total plates with umbra intersection: 7
 
View attachment 83183
From the 5399 sources. I get 39 In Shadow according to this code.

Python:
import csv
import os
import glob
from astropy.io import fits
from astropy.time import Time
from earthshadow import get_shadow_center, get_shadow_radius, dist_from_shadow_center
from astropy.coordinates import Angle
import astropy.units as u
from astropy.coordinates import SkyCoord

import matplotlib.pyplot as plt
import numpy as np

def fits_time( date_obs, ut ) :
    date_split = date_obs.split( "/" )
    return f"19{date_split[2]}-{date_split[1]}-{date_split[0]}T{ut}"

def in_shadow( ra_value, dec_value, time ) :

    center = get_shadow_center( time, obs='Palomar', orbit='GEO')
    radius = get_shadow_radius(orbit='GEO')
    dist   = dist_from_shadow_center( ra_value, dec_value, time=time, obs='Palomar', orbit='GEO')
    return dist < radius - 2*u.deg

def get_metadata():
    output_csv_file = "extracted_data.csv"
    metadata_list = []
 
    fits_files = glob.glob('fits_files/*.fits')
 
    for fits_file in fits_files:

        with fits.open(fits_file) as hdul:

            header = hdul[0].header

            c = SkyCoord( header["OBJCTRA"], header["OBJCTDEC"], unit=(u.hourangle, u.deg), frame='icrs')
         
            ra_deg  = c.ra.degree
            dec_deg = c.dec.degree

            metadata = {
                "file_name" : os.path.basename(fits_file),
                "DATE-OBS"  : header["DATE-OBS"],
                "UT"        : header["UT"],
                "SITELAT"   : header["SITELAT"],
                "SITELONG"  : header["SITELONG"],
                "OBJCTRA"   : header["OBJCTRA"],
                "OBJCTDEC"  : header["OBJCTDEC"],
                "EQUINOX"   : header["EQUINOX"],
                "EPOCH"     : header["EPOCH"],
                "EXPOSURE"  : header["EXPOSURE"],
                "SHADOW"   : in_shadow(
                    ra_deg,
                    dec_deg,
                    time=Time( fits_time( header["DATE-OBS"], header["UT"] ), format='fits') )[0]
            }

            metadata_list.append( metadata )

    with open(output_csv_file, 'w', newline='') as csvfile:
        fieldnames = list(metadata_list[0].keys())
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(metadata_list)

if __name__ == "__main__":
    get_metadata()

I am not sure how to figure out how many are expected to be accounting for location bias in the POSSI data, and fraction of the sky in the shadow etc.
I am I correct in that 0.72% of detections are within earth's shadow?

If so, and if my code is correct, the shadow method is entirely unconvincing, at least for geostationary orbits.
 
Last edited:
I am I correct in that 0.72% of detections are within earth's shadow?

If so, and if my code is correct, the shadow method is entirely unconvincing, at least for geostationary orbits.

0.55% is close to the actual percent of the sky in Earth shadow at GEO. It's also around what you will likely get if you have errors in how you extract and use coords or times, since that will tend to undo whatever correlation you would otherwise have from the detections or plate collection biases.

For example, if I randomize the times, I get around that value. That's an indication the shadow calculation might be reasonably accurate, but a spatiotemporal collection bias needs to be accounted for.

Make sure you also use precisely the right altitude (e.g. don't add Earth radius to the altitude), and then make sure you look at all plates. A number around 0.55% but off by some significant amount, is more likely to be a correct value that very close to 0.55% since there is likely some spatiotemporal bias.

Although major warning here, I am not sure this is all correct. A lot more diligence would be required to produce a truly reliable analysis.
 
Last edited:
0.55% is close to the actual percent of the sky in Earth shadow at GEO. It's also around what you will likely get if you have errors in how you extract and use coords or times, since that will tend to undo whatever correlation you would otherwise have from the detections or plate collection biases.

For example, if I randomize the times, I get around that value. That's an indication the shadow calculation might be reasonably accurate, but a spatiotemporal collection bias needs to be accounted for.

Make sure you also use precisely the right altitude (e.g. don't add Earth radius to the altitude), and then make sure you look at all plates. A number around 0.55% but more off is more likely to be a correct value since there is likely some spatiotemporal bias.
I've come to the conclusion that astrophysics is hard.

I'll just stick to trying to find transients/defects within the red/blue plates and let someone else deal with the hard maths lol.

I'll be posting the CSVs for the red data when they've all downloaded in case anyone wants to give it a shot.
 
I have a google drive link now.

I am starting to add the red images, but I do have all the meta data snippits up.

Edit: I originally intedened to compress the images to a tarball. Its been 10 hours since I started that and it didn't even get 1/3 of the way through. I'm just uploading them as is instead.
 
Last edited:
Back
Top