Visualization of QZSS (Quasi-Zenith Satellite System) trajectories observed from Hirosaki, Japan

Hello everyone,

I’m currently struggling to decode signals from some of the satellites I’m tracking in the SatNOGS community, so in the meantime, I’ve been having some fun experimenting with GPS data.

I’d like to share a recent visualization of satellite data I’ve been tracking from my station in Hirosaki, Japan.

Using the data accumulated over time, I’ve plotted the trajectories of GNSS satellites. In this skyplot, the red dots represent the Quasi-Zenith Satellite System (QZSS: PRN 194, 195, 196), while the blue dots represent standard GPS satellites.

You can clearly see the characteristic “figure-eight” orbit of the QZSS as it dwells over the Japanese region. It’s fascinating to compare this with the broader movement of standard GPS satellites.

I am using this data to better understand the satellite signal environment in my location. I hope this visualization is of interest to the community!

Best regards, Yukio

4 Likes

what app and device you use?

1 Like

Hi bali, thanks for your interest!

I’m using a simple USB GPS dongle (u-blox 7 chipset) connected to an older laptop running Linux. It’s placed right by the window to get a clear view of the sky. For tracking the GPS data, I’m running some custom scripts on the system to log the satellite positions (azimuth and elevation) and then using Python (matplotlib) to visualize them.

It’s a fun way to get familiar with the satellite environment while I’m still learning the ropes of decoding other signals!

Best regards, Yukio

To follow up on my previous post about the skyplot, I’ve been analyzing the accumulated observation data from my station.

I’ve created a new graph showing the total observation counts (where elevation > 10 degrees) for all visible GNSS satellites. As you can see, the QZSS satellites (PRN 194, 195, 196) show significantly higher counts compared to the standard MEO GPS satellites.

This data effectively visualizes the “quasi-zenith” nature of the QZSS constellation—their tendency to remain at high elevation over Japan for extended periods, rarely dropping below the horizon. It’s been a fascinating way to verify how these satellites behave compared to the rest of the GNSS constellation in my local sky.

Best regards, Yukio

2 Likes

Hi everyone,

Following up on my previous posts regarding my station’s skyplot and observation counts, I’ve conducted further analysis to visualize the “quasi-zenith” nature of the QZSS constellation.

I’ve created a stacked histogram showing the elevation angle distribution of all visible GNSS satellites (attached below).

Key insights from the data:

  • QZSS (PRN 194, 195, 196) Concentration: As expected, the green bars (QZSS) show a significant concentration in the 75°–90° elevation range. This clearly demonstrates their “quasi-zenith” behavior, remaining high in the sky over Hirosaki for extended periods.

  • Excellent Horizon Visibility: The blue bars (MEO satellites) show a high frequency of data even at low elevation angles (0°–10°). This confirms that my station has a very clear, unobstructed view of the horizon, allowing for stable tracking of satellites as they rise and set.

  • Stacked Distribution: By comparing the stacked densities, it’s fascinating to see how the QZSS satellites dominate the zenith area, effectively “topping off” the distribution of the standard MEO constellation.

This analysis has been a great way to verify my station’s “character” and the performance of my current setup (u-blox 7 dongle).

I’m curious—do any of you see similar patterns in your respective regions?

Best regards, Yukio

1 Like

I also performed a statistical analysis on the observation frequency by PRN (attached). It is striking to see the difference between the MEO GPS constellation and the QZSS. As shown, the QZSS (PRN 194, 195, 196) counts are nearly triple that of the average GPS satellite, visually confirming their persistent presence directly above my station in Hirosaki. This data effectively highlights the “quasi-zenith” advantage in real-world reception performance.

1 Like

is the script open source? maybe you can share here so we can try too. or share the link the software

1 Like

Hi Bali, thanks for the kind words!

As I mentioned, I am a complete beginner in Python. I developed these scripts through a collaborative process with Gemini (my AI collaborator). I’m still learning how they work under the hood, so please treat these as “experimental” and use them at your own risk.

I’m happy to share the code in the spirit of open learning. Here is the script I used to generate the skyplot:

Python

import matplotlib.pyplot as plt
import numpy as np

# This script loads GPS/QZSS data and generates a skyplot
def load_data(prn_target):
    azims, elevs = [], []
    # Note: Ensure the path to your log file is correct
    with open("qzss_project/gps_trajectory.log", "r") as f:
        for line in f:
            parts = line.strip().split(',')
            if len(parts) == 3 and parts[0] == prn_target:
                azims.append(np.radians(float(parts[1])))
                # Polar plot maps elevation 0-90 to radius 90-0
                elevs.append(90 - float(parts[2]))
    return azims, elevs

fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot(111, projection='polar')
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)

# Automatically identify all PRNs in the log file
all_prns = set()
with open("qzss_project/gps_trajectory.log", "r") as f:
    for line in f:
        all_prns.add(line.split(',')[0])

for prn in sorted(all_prns, key=int):
    az, el = load_data(prn)
    if az:
        ax.scatter(az, el, s=1, label=f'PRN {prn}')

ax.set_title("GPS & QZSS Satellite Skyplot")
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.savefig("gps_skyplot.png")
print("Successfully generated gps_skyplot.png")

I hope this helps! I’m still figuring out how to improve the logging and data processing, so if you have any suggestions or find a more efficient way to write this, please let me know—I’d love to learn from you all.

Best regards, Yukio

1 Like

Hello everyone,

I would like to share my recent experience regarding my GPS module (u-blox 7) performance.

For the past several weeks, I have been running a fixed-point observation at my study room window (south side). Today, the device went down due to the extreme heat and high humidity (local weather conditions were severe).

Initially, the device failed to get a GPS fix even though it was recognized by the system. I suspected the heat and humidity were causing signal noise and processing issues.

What I did: I closed the window (due to heavy rain) and turned on the air conditioner to stabilize the room environment.

Result: The SNR improved significantly after the room temperature stabilized. Even with the window closed during heavy rain, I am now achieving an SNR of 36 and a stable green lock status in xgps.

It seems that even for a small GPS module, thermal/humidity stability is critical for consistent signal processing. I hope this helps others who might be struggling with similar observation stability issues.

Best regards, Yukio

1 Like

To follow up on the stability tests, here is a visualization of the GNSS satellite visibility over different time periods (1, 3, 6, 12, and 24 hours) generated using Python scripts. As you can see, the longer the observation time, the more dense and complex the orbital tracks become. This comprehensive tracking helps in understanding satellite availability and optimizing receiver performance over a full diurnal cycle. YUKIO

1 Like

To follow up on the comparison across different time scales, here is the Python script used to generate the 1-hour trail plot (plot_1h_trails.py).

You can easily adjust the time window by changing the Timedelta hours (e.g., hours=1, 3, 6, 12, 24) to observe how the individual satellite passes accumulate into a full diurnal cycle:

Python

import json
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# 1. Load and parse data
data_file = "gps_parsed.json"
records = []

with open(data_file, "r") as f:
  for line in f:
    try:
      obj = json.loads(line.strip())
      if obj.get("class") == "SKY" and "satellites" in obj:
        t = obj.get("time")
        for sat in obj["satellites"]:
          records.append({
              "time": t,
              "PRN": sat.get("PRN"),
              "az": sat.get("az"),
              "el": sat.get("el"),
              "used": sat.get("used"),
          })
    except json.JSONDecodeError:
      continue

df = pd.DataFrame(records)
df = df.dropna(subset=["az", "el", "time"])
df["time"] = pd.to_datetime(df["time"])
df = df.sort_values("time")

# 2. Filter for the desired time window (e.g., Past 1 Hour)
latest_time = df["time"].max()
start_time = latest_time - pd.Timedelta(hours=1)  # Change hours here (1, 3, 6, 12, 24)
df_filtered = df[df["time"] >= start_time]

print(f"Latest Time: {latest_time}")
print(f"Start Time: {start_time}")
print(f"Total Records: {len(df_filtered)}")

# 3. Plotting using polar coordinates (Skyview)
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": "polar"})
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)

for prn, group in df_filtered.groupby("PRN"):
  az_rad = np.deg2rad(group["az"])
  r = 90 - group["el"]
  ax.plot(az_rad, r, label=f"PRN {prn}", alpha=0.7, linewidth=1.5)

ax.set_ylim(0, 90)
ax.set_yticks([0, 30, 60, 90])
ax.set_yticklabels(["90°", "60°", "30°", "0°"])
ax.set_title(
    f"Satellite Trails (Past 1 Hour)\n~ {latest_time.strftime('%Y-%m-%d %H:%M')}",
    va="bottom",
    fontsize=13,
)

ax.legend(
    loc="upper right",
    bbox_to_anchor=(1.3, 1.1),
    fontsize=8,
    ncol=1,
    markerscale=0.8,
)

plt.tight_layout()
plt.savefig("satellite_trails_1h.png", dpi=300)
print("Saved plot to satellite_trails_1h.png")

Feel free to try it out and tweak it for your own station monitoring! 73, YUKIO

To follow up on the satellite visibility and trajectory tracking, here is a new addition: the time-series variation of Signal-to-Noise Ratio (SNR) for the tracked GNSS/QZSS satellites over a 48-hour period.

As you can see, the SNR traces form distinct arch-like trajectories that correlate perfectly with the orbital motion of each satellite (rising from the horizon, peaking at high elevation, and descending). This data helps evaluate local horizon obstructions and propagation conditions at the station.

Feel free to check out the trend. 73, YUKIO

1 Like

I noticed that your Gemini AI assistant helped you come up with script to plot & anticipate satellite trajectories. I wonder if your assistant has any idea on how to trace back a satellite signal received ( IQ data) to the satellite transmitting the signal ? I’ve narrowed down the signal to a Geo sat, cw. Continuous Wave frequency. 0 Hz range. I’m using the ( RTL-SDR v4 Dongle ) RF Analyzer, app.

Theoretically, maybe it would possible to run the script on a known sat in the region. Plugin the IQ data of the mystery signal & run the script to do a comparison. Or maybe Gemini could do a real time comparison ?

Cheer’s

Bali, Indonesia

1 Like

Hi CyborgSami,

Haha, to be completely honest, since I’m just exploring all of this with the help of my AI assistant (Gemini), I wasn’t entirely sure what your question meant at first!

So, I just threw your question straight back to Gemini to see what it would say. Here is the response it gave me:

"Regarding tracing back an unknown or mystery CW signal from a geostationary satellite using IQ data: Yes, it is theoretically possible, but it usually requires a combination of orbital mechanics and signal analysis. Since GEO satellites remain at fixed nominal positions relative to the Earth, you can cross-reference the exact reception time, Doppler shift, and known beacon or telemetry frequencies listed in coordination databases (like Satbeams or spacecraft frequency allocations) for that specific orbital slot.

Alternatively, comparing the raw IQ stream against a known reference signal or running a cross-correlation can help pinpoint matching telemetry characteristics. It’s a fascinating challenge for an RTL-SDR setup!"

Does this make sense for your RTL-SDR setup in Bali? Let me know what you think!

73, YUKIO

Maybe. If your assistant can write script for you, I thought perhaps your assistant could compile recorded IQ data and narrow down the signal to a known satellite frequency or orbit . I suspect the signal is from the satellite flying a figure 8 around Bali. There are nearly a dozen geo sats in the region. I suppose the signal could be from any of them.

1 Like

1 Like

Hi CyborgSami,

Absolutely, let’s do it! My AI assistant (Gemini) is all fired up and eager to help with this.

To tackle your mystery GEO/inclined-orbit signal from Bali, here is a proposed game plan that Gemini suggested. What do you think about this step-by-step approach?

  1. Data Format Check: Clarify what format your recorded IQ data or logs are in (e.g., CSV, raw binary, JSON) so the script can parse them correctly.

  2. Ephemeris & Position Matching: Use Python (with libraries like Skyfield or SGP4) along with TLE data to calculate the positions and potential Doppler characteristics of candidate satellites (like USA 346 and others near your longitude) from Bali’s coordinates (approx. 8.7°S, 115.1°E).

  3. Cross-Correlation & Scoring: Compare the frequency/time signatures of your recorded signal against the calculated orbital dynamics to rank the candidates.

Let me know if this sounds like a good direction, or if you have any specific format for your data!

73, YUKIO

Hi CyborgSami,

While waiting for your reply, my impatient AI assistant (Gemini) couldn’t sit still and decided to draft a prototype script anyway!

It uses Python and the skyfield library to calculate the position and expected Doppler shift of inclined GEO/GSO satellites (like USA 346) relative to your coordinates in Bali (approx. -8.7°, 115.1°). Here is what it whipped up:

Python

from datetime import datetime, timezone
import numpy as np
from skyfield.api import EarthSatellite, Topos, load

# 1. Define Observer (Bali, Kuta Beach area)
ts = load.timescale()
bali = Topos(latitude_degrees=-8.71962, longitude_degrees=115.16935)

# 2. Load TLE for a target satellite (e.g., USA 346 / placeholder TLE)
# Note: Replace with actual TLE data from Celestrack
line1 = (
    "1 56163U 23031A   26212.50000000  .00000000  00000-0  00000-0 0  9999"
)
line2 = (
    "2 56163  12.3100   0.0000 0010000   0.0000   0.0000  1.00270000  1234"
)
sat = EarthSatellite(line1, line2, "USA 346", ts)

# 3. Calculate position and range-rate (Doppler shift indicator) over time
t0 = ts.now()
difference = sat - bali
topocentric = difference.at(t0)

alt, az, distance = topocentric.altaz()
range_rate = topocentric.rate_of_change(bali)

print(f"Time (UTC): {t0.utc_strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Altitude: {alt.degrees:.2f} deg, Azimuth: {az.degrees:.2f} deg")
print(f"Range Rate (Velocity towards observer): {range_rate.km_per_s:.4f} km/s")

Of course, this is just a rough draft. Once you let us know what your data format looks like (or if you have specific TLEs), we can hook it up to parse your IQ/log files.

Let us know what you think!

73, YUKIO

1 Like

Yes. For sure, let’s solve the mystery origin of the satellite signal together. Step by step seems like a logical approach. The data I recorded are raw IQ files. ‘.IQ’ ( Inphase Quadrature ). “RF Analyzer” app. Another file type recorded was, ‘IQ.WAV’ from the “SRD Touch” app. I have other apps which may allow me to save in different file types.

How large of a file recording does your assistant recommend to make a computation ? The IQ files i recorded average 100mb +/-. Approximately, a 1 minute recording. I assume a larger file will give us a more straight line picture of orbital parameters. Due to current file storage capacity , perhaps taking multiple 100mb recordings during a 24 hour period would allow your assistant to connect the dots and render a trajectory. I can uploaded the data files to the cloud & post the link. My tablet file storage is limited to about 30gb. Cloud storage 5gb.

Cheer’s

1 Like

Hi CyborgSami,

That sounds like a solid plan! Capturing multiple 100MB chunks of raw IQ data (via RF Analyzer or SRD Touch) and sharing them via cloud storage fits well within your storage limits, and it should be more than enough to track the trajectory over time.

To process those .IQ or .IQ.WAV files, my AI assistant suggests writing a Python script using NumPy and SciPy to perform an FFT (Fast Fourier Transform), detect the peak frequency for each time chunk, and plot the Doppler curve.

Whenever you’re ready, feel free to drop a link to a sample IQ file here (or a snippet of your log). Once we can look at the data structure, we’ll tweak the script to parse it directly and start matching it against those satellite candidates!

Cheers, YUKIO