Why Do Galaxies Spin the Way They Do?

For nearly a century, astronomers have observed a quiet but persistent contradiction: galaxies spin too fast. The outer stars move as if held by invisible hands—far beyond what visible matter should allow. According to Newton’s law of gravity, these stars should slow down with distance. But they don’t.

Instead, the curves flatten. The velocity remains high, long after the luminous matter ends. This mismatch is one of modern astronomy’s most famous puzzles, and it has a name: the galactic rotation problem.

Most models solve this by adding something we can’t see: dark matter. But what if the stars are not lying—and nothing is missing? What if the problem is not the mass… but the math?

A Mystery Since 1933: Zwicky, Rubin, and the Rotating Sky

In 1933, Swiss astrophysicist Fritz Zwicky studied the Coma Cluster and made a shocking discovery: galaxies within the cluster were moving far too fast to be held together by the visible matter. To account for this, he proposed the existence of “dunkle Materie”—dark matter—an invisible substance whose gravity could explain the motions.

Zwicky’s proposal was bold, and largely overlooked. But in the 1970s, another scientist brought the mystery back into the spotlight—this time, within individual galaxies. Vera Rubin, working with W. Kent Ford, carefully measured how stars moved in spiral galaxies like Andromeda. What she found stunned the astrophysics community: the rotation curves didn’t fall. They flattened.

Rubin’s data was meticulous and unambiguous. It showed that Newtonian expectations failed—consistently and dramatically. The case for dark matter was no longer speculative; it was empirical. And yet, Rubin herself remained open-minded about the nature of the cause. In her words:

“If I could have my pick, I would like to learn that Newton’s laws must be modified in order to correctly describe gravitational interactions at large distances.”
— Vera Rubin (1980)

Spatial-Causal Geometry (SCG) enters not as a rejection of Rubin and Zwicky, but as a possible resolution to the very problem they saw so clearly. It offers a third path: not added matter, not tweaked force—but a change in perspective. What if the stars aren’t misbehaving? What if the space around them is telling the truth?

What SCG Says: It’s Not Mass That Moves Stars—It’s Geometry

Most models of galactic motion begin with force: how much mass is there, and how strongly does it pull? But Spatial-Causal Geometry (SCG) starts elsewhere. It begins with a different question:

What if motion isn’t caused by mass at all?
What if motion emerges from how space itself curves?

In SCG, space is not empty. It has structure—a causal-density field, written as \( \rho(x) \). This field tells us how “tightly woven” space is at every point. And where this field curves, things move. No invisible matter, no pulling forces—just local imbalances in geometry, like a ball rolling down a slope.

Instead of force causing acceleration, SCG says acceleration arises from the gradient of this curvature:

\( a(x) = c^2 \cdot \frac{d}{dx} \ln \rho(x) \)

Here, \( c \) is the causal speed limit—analogous to the speed of light—and \( \ln \rho(x) \) is the natural logarithm of the density field. When \( \rho(x) \) is flat, there’s no motion. When it curves, structures respond by accelerating. That’s all it takes.

This is the heart of SCG: movement is not commanded; it is permitted by curvature. Galaxies spin not because something is pulling on them, but because their space is curved in a very specific, very consistent way.

The SCG Rotation Curve Equation: Just One Line

If acceleration comes from the curvature of \( \ln \rho(x) \), then rotation—circular motion—emerges naturally from that geometry. In Spatial-Causal Geometry, the orbital velocity of a star at radius \( r \) is given by:

\( v(r) = A \cdot r^{(1 - B)/2} \)

This deceptively simple equation has only two free parameters:

To evaluate how well a curve fits a galaxy, we calculate the root-mean-square deviation (RMSD). This is a measure of the average error between the predicted velocities and the observed ones. The smaller the RMSD, the better the fit.

In SCG, causal phase structure refers to how the spatial curvature evolves as you move outward from the center of a galaxy. It's the pattern or “tempo” of how tightly or loosely space is curved, and how that pattern influences motion. When \( B \) is higher, curvature drops off more sharply—leading to falling rotation curves. When \( B \) is lower, curvature is shallower and more extended—resulting in flat or rising curves.

This structure isn’t imposed by external forces—it emerges from how space itself wants to bend. And \( B \) lets us read that curvature’s character directly from the stars.

Unlike Newtonian gravity, where velocity falls off as \( v \sim r^{-1/2} \), or dark matter models that require complex halo profiles, SCG gives you the full rotation curve from geometry alone. It doesn’t need to be patched with invisible mass. It just asks:

How does space curve here?

When you fit this curve to real galactic data, you’re not tuning a theory—you’re tracing the shape of space.

A Real Example: Fitting UGC 05829 with SCG

To see SCG in action, let’s look at a relatively quiet galaxy: UGC 05829. It’s a late-type spiral with a smooth rotation profile and just enough data points to resolve its causal structure clearly. This makes it a perfect first example for a single-phase SCG fit.

Below, the blue points show real observations of stellar velocity at different radii. The orange line is the SCG prediction, generated from the galaxy’s own geometric curvature:

\( v(r) = A \cdot r^{(1 - B)/2} \)

SCG fit for UGC 05829

For UGC 05829, the SCG model found:

Despite the simplicity—just a single phase and two parameters—the fit is remarkably precise. The low value of \( B \) means the causal gradient is nearly flat, which translates into a slow and steady rise in velocity across the disk.

This is the elegance of SCG: no added mass, no special forces. Just the curve of space—and how the stars respond to it.

How We Fit the Curve: From Data to Geometry

Every galaxy fit starts the same way: with a table of observations. For each radius \( r \), we’re given the orbital velocity \( v \)—how fast the stars are moving at that distance. This is the raw rotation curve.

SCG doesn’t simulate particles or halos. It simply asks: what geometric curve best describes these points, based on the equation:

\( v(r) = A \cdot r^{(1 - B)/2} \)

To find the best-fitting values of \( A \) and \( B \), we use a standard method from numerical analysis: nonlinear least squares optimization. Specifically, we minimize the root-mean-square deviation (RMSD) between the observed velocities and the predicted curve.

This means we’re solving:

\( \text{RMSD} = \sqrt{ \frac{1}{N} \sum_{i=1}^{N} (v_{\text{obs},i} - v_{\text{SCG}}(r_i; A, B))^2 } \)

We scan through candidate values of \( A \) and \( B \), adjust them automatically, and stop when the RMSD is as low as possible. The lower the RMSD, the tighter the match. For most of the 175 galaxies studied, SCG fits converge in milliseconds—fast, stable, and accurate.

When the galaxy is simple, like UGC 05829, a single curve is enough. When it’s more complex, we test for a breakpoint and consider a two-region model. But the principle is always the same: match the observed motion by finding the shape that space wants to be.

When One Curve Isn’t Enough: Dual-Region SCG Fitting

Not all galaxies follow a single smooth pattern. Some change character—gently or suddenly—as you move outward from the core. In SCG, these transitions are called causal phase shifts, and they show up as changes in the curvature of space itself.

To model these galaxies, we extend the basic SCG equation into two regions, each with its own parameters:

The point where the galaxy transitions from one curvature profile to another is called the breakpoint. We don’t choose it by eye—we find it by scanning every possible location and computing the total RMSD across both regions.

If the dual-fit RMSD improves by at least 20% compared to the single fit, we accept the dual model as a meaningful phase separation.

This isn’t just an algorithmic trick—it’s a geometric insight. A galaxy that changes causal structure is telling us something about how its inner and outer densities evolved. SCG listens carefully, and models both regions with precision.

Case Study: NGC 2903 — Two Curves, One Galaxy

NGC 2903 is a bright barred spiral galaxy, rich in structure and star formation. When we first attempt to fit it with a single SCG curve, the result is decent—but something feels off. The inner velocities rise too steeply, while the outer disk flattens more gently. A single causal phase can’t capture both behaviors at once.

So we let SCG ask a deeper question: is there a natural place to divide the curve? Using a breakpoint scan, the model splits the galaxy into two regions and fits each independently. The result is strikingly better.

SCG dual-region fit for NGC 2903

SCG identifies the breakpoint at \( r = 2.88 \, \text{kpc} \), and returns these parameters:

That’s an 84% improvement in RMSD—well beyond the 20% threshold. More importantly, the fit aligns beautifully with observation. The inner region reflects a gently curved basin with a lower causal-phase exponent \( B_1 = -0.337 \), while the outer region sharpens into a steeper curvature phase with \( B_2 = 1.184 \). This dual-profile structure captures the shift from central coherence to an extended outer gradient with remarkable fidelity.

This is the power of SCG’s dual-region modeling: it lets space speak with two voices, and listens to both.

Click any “View” link to display the SCG rotation curve fit for that galaxy. Click column headers to sort.

GalaxyDatapointsA1B1Single RMSDBreak RadiusA2B2Dual RMSDView
CamB911.863-0.8590.271View
D512-2425.3480.3822.298View
D564-8614.605-0.0461.127View
D631-71620.057-0.2932.8744.94046.8430.7771.286View
DDO0641427.554-0.6243.7871.68043.3220.8411.597View
DDO1541222.185-0.2073.0543.46042.6010.8840.889View
DDO1613121.375-0.0362.7898.16046.3210.7090.993View
DDO1681028.320-0.0443.775View
DDO170827.6030.3153.603View
ESO079-G0141541.854-0.2039.3078.340100.5150.5834.402View
ESO116-G0121545.409-0.0907.3584.13089.2780.8011.557View
ESO444-G084739.9920.2774.072View
ESO563-G0213043.083-0.80841.64410.230287.5200.9466.866View
F561-1624.4070.2983.466View
F563-11720.090-1.50010.6926.95065.0620.6302.948View
F563-V1614.6670.2723.159View
F563-V21053.0990.25313.111View
F565-V2724.423-0.1724.060View
F567-2526.0400.3574.065View
F568-11235.850-0.59510.2874.84079.3630.5531.191View
F568-31818.592-1.0629.6994.19053.6020.4592.339View
F568-V11542.727-0.23511.9575.090101.5170.9052.760View
F571-81342.833-0.1298.0245.44075.9030.5123.366View
F571-V1731.8240.1965.576View
F574-11432.078-0.3747.6255.17067.5110.6831.463View
F574-258.385-0.3340.991View
F579-V11464.5030.2238.8054.74093.4340.8413.580View
F583-12521.593-0.4917.8475.40053.6580.6452.609View
F583-41229.9340.0482.5214.35033.5990.2741.960View
IC25743413.938-0.3681.907View
IC42023263.040-0.44327.2987.080231.9360.9677.047View
KK98-2511516.166-0.8452.1401.72024.4480.3520.611View
NGC00242970.263-0.17314.4152.020101.7360.9474.007View
NGC00552126.763-0.1455.2907.98077.3990.9121.098View
NGC01002128.752-0.4325.5533.88053.3410.5351.269View
NGC02472640.5730.2322.84410.23070.8470.6872.113View
NGC028928180.8751.0349.12337.840414.1871.4427.238View
NGC03002536.899-0.1835.4834.54062.7020.6562.073View
NGC080113121.196-0.16027.72410.590249.4471.0737.126View
NGC089118216.8730.9789.2289.730325.7651.3096.381View
NGC10033644.7270.1835.3008.70067.0260.6912.219View
NGC10902456.245-0.25121.2016.610182.1031.0653.753View
NGC17051467.2130.6493.7392.45072.3851.0161.063View
NGC23662622.794-0.5275.1752.97051.5691.0261.402View
NGC24037372.9450.3428.2823.900106.8090.8393.343View
NGC268311208.0081.06014.25717.310398.4961.55810.693View
NGC284150269.8270.84411.83914.350362.3101.1285.670View
NGC290334114.661-0.33731.9002.880243.1541.1845.085View
NGC29153039.105-0.5039.6293.35078.0490.9472.316View
NGC295524203.7300.76719.56319.490677.9461.60110.924View
NGC29762752.160-0.7693.7331.56069.3330.4191.939View
NGC299813125.6880.30319.0197.590210.1190.9974.706View
NGC31092519.467-0.8652.7582.58028.5200.0520.837View
NGC31984353.224-0.07219.1978.040158.3361.0332.895View
NGC352141178.3220.50012.1452.230222.2121.0473.627View
NGC37261260.9790.13011.55712.190120.7380.8125.252View
NGC37412124.6780.2521.4552.80028.8890.3911.074View
NGC37691278.6260.5428.32110.470127.1011.0462.682View
NGC38771353.005-0.51416.5135.240141.5620.8394.342View
NGC389310163.0980.91412.439View
NGC39171736.512-0.40612.8956.980119.2600.8893.627View
NGC39497115.4930.5973.421View
NGC39538167.2710.7747.898View
NGC39721056.3880.1495.737View
NGC39929296.5841.09811.436View
NGC40101240.019-0.1517.7916.110108.4940.8733.212View
NGC401336230.5381.1717.52914.980113.4850.7342.634View
NGC40517106.3250.6688.984View
NGC4068620.746-0.7070.904View
NGC4085762.2080.05510.353View
NGC40881275.0050.13217.10410.470199.0531.1085.548View
NGC41002481.8460.07417.5037.850307.6591.4104.879View
NGC41387218.1701.25511.231View
NGC41571789.2250.18418.77810.470204.4081.0665.710View
NGC41832352.9340.2497.9247.850116.0981.0351.972View
NGC42141467.8130.7222.1052.30078.1860.9630.658View
NGC42171967.131-0.37821.4095.240192.0731.0386.778View
NGC4389633.771-0.4141.851View
NGC45593254.2860.0667.2074.58091.9570.7972.705View
NGC500518225.4230.8495.2193.440251.6520.9562.422View
NGC503322214.2550.98110.36830.870319.5121.2703.960View
NGC505528150.4690.67616.83611.490283.0821.2464.757View
NGC537119205.4490.91312.15831.760211.3791.0055.420View
NGC55852439.3290.1065.1556.85094.0751.0342.479View
NGC590719188.8990.8377.06517.610292.0971.1643.840View
NGC598533154.8540.42614.20111.080333.9001.0873.777View
NGC60154480.243-0.01615.6243.280143.2050.9405.257View
NGC619523183.7520.61311.7776.340226.3090.9357.930View
NGC65033186.4180.4576.1594.550120.0071.0261.486View
NGC667415206.7360.78720.75927.480284.1641.08210.492View
NGC6789478.674-0.5590.614View
NGC694658139.2740.92112.4764.770216.6021.2016.010View
NGC733136181.2210.5567.2965.350275.5991.0913.898View
NGC77934663.4460.14711.0554.200195.1441.7064.534View
NGC781418255.6311.1302.60912.820214.0001.0001.803View
PGC51017619.5761.1010.222View
UGC001282242.2890.18610.36716.220114.6310.9334.093View
UGC00191944.8910.3966.868View
UGC00634447.4910.4005.179View
UGC007311233.3250.1992.6286.36053.5700.7181.292View
UGC00891523.062-0.0622.893View
UGC012301132.845-0.28917.8388.600113.9221.0495.400View
UGC012812520.393-0.7933.8463.07045.1350.7090.535View
UGC02023519.244-0.6680.575View
UGC02259860.6010.6022.805View
UGC02455821.773-0.3902.545View
UGC0248717380.6401.01311.65845.210448.1671.1435.405View
UGC0288519278.6811.00411.74231.220200.7640.8117.658View
UGC0291643201.6390.9459.81022.140666.9621.7284.468View
UGC02953115241.4130.87025.6925.250362.6331.1619.147View
UGC0320548146.3430.39322.5614.970259.6901.1163.599View
UGC0354630240.7941.2217.1279.730243.1691.1484.126View
UGC035804770.1080.8655.1022.18076.1070.6713.100View
UGC042782528.900-0.0303.1674.01024.769-0.4041.815View
UGC043052222.928-1.3245.2991.50035.8401.0881.432View
UGC04325860.0330.4127.041View
UGC04483823.8670.0571.572View
UGC04499935.7320.2404.197View
UGC050051119.040-0.2986.1348.60042.1360.4572.662View
UGC0525373240.4920.9709.03518.340408.1601.3352.880View
UGC05414628.905-0.1081.921View
UGC057161239.0210.3952.3127.23065.8810.9080.538View
UGC057212364.3830.0859.2091.53083.6311.0741.727View
UGC057501111.003-0.9638.9664.82039.8290.5213.113View
UGC057641038.5120.3824.891View
UGC058291122.489-0.1541.120View
UGC05918825.5950.2131.770View
UGC059861543.552-0.36911.7304.390127.6141.1282.183View
UGC05999531.8120.1445.924View
UGC06399936.7720.0905.107View
UGC064461746.9690.3254.6075.23076.1720.9242.572View
UGC0661413211.3281.16610.79424.940159.3240.8752.780View
UGC06628730.2680.6202.331View
UGC06667936.4350.0954.667View
UGC0678645186.2790.8337.92914.930287.4941.1633.366View
UGC0678771262.0910.71118.4871.660224.1240.94611.700View
UGC06818822.017-0.2964.295View
UGC069171146.3210.0753.8306.11067.2610.5761.327View
UGC06923643.6370.1853.697View
UGC069301060.1950.5426.169View
UGC069739165.9120.9392.581View
UGC069831758.0090.3776.4177.850109.1791.0013.631View
UGC070891224.436-0.2072.3745.24036.3980.3031.258View
UGC071251326.9070.2613.41411.47064.6670.9941.502View
UGC071511142.076-0.1325.2943.00053.4420.5902.022View
UGC07232450.533-0.4640.295View
UGC07261751.5010.5602.398View
UGC073231031.965-0.1561.754View
UGC073991065.2300.4404.275View
UGC075243127.974-0.1123.5573.79044.6870.4771.111View
UGC07559719.879-0.1611.333View
UGC07577911.455-0.5870.305View
UGC076031240.940-0.2814.6282.05056.2340.8350.667View
UGC07608828.607-0.1833.168View
UGC07690753.6420.8544.009View
UGC07866722.6200.0870.809View
UGC082861744.064-0.2986.6922.84066.1770.7581.655View
UGC084903059.4600.1965.3602.03077.2260.9801.104View
UGC085501138.6510.2683.2032.92042.2570.6142.043View
UGC0869941188.3030.7489.9082.390190.1541.0375.082View
UGC08837814.868-0.7221.751View
UGC090372256.2400.25710.87618.230292.3681.4105.004View
UGC0913368279.6190.98313.91516.610382.5301.2343.858View
UGC09992529.3370.7590.361View
UGC10310740.0560.3474.801View
UGC114553670.456-0.11235.08514.560367.6831.1748.545View
UGC115571219.106-0.7658.1194.62057.3280.6643.078View
UGC118201034.0300.3224.613View
UGC1191465266.1430.30417.3211.300290.8921.0123.709View
UGC1250631128.6330.52318.44618.850351.0511.2165.657View
UGC126321528.833-0.1303.6114.26046.5730.6050.885View
UGC127321636.8000.1532.5155.76044.1310.4381.647View
UGCA281730.6190.0211.731View
UGCA442828.0010.1293.797View
UGCA4443623.388-0.2441.1990.69024.6860.1260.650View

How Does SCG Compare?

While Spatial-Causal Geometry approaches galactic motion from geometric principles—not through conventional force models—it’s still fair to ask: how well do its predictions match real data?

Below is a summary of RMSD (root-mean-square deviation) values reported by various models across similar galactic datasets. Lower RMSD indicates a closer match between model and observation.

Study / Model Typical RMSD (km/s)
McGaugh et al. (2016, 2020) 6–13
Li et al. (2018) ~13
NFW / Burkert / MOND fits 6–15
D. J. Hallman (2025, SCG) 0.22–13.1 (most between 2–5)

These results speak for themselves. SCG isn’t just a different way of seeing—it’s also surprisingly precise. And all it needs is geometry.

Appendix: SCG Fit Code (Python)

This script applies the SCG causal-density velocity model to galactic rotation data. It evaluates both single and dual-region fits, selecting the model with the lowest RMSD. The equation used is \( v(r) = A \cdot r^{(1-B)/2} \), derived from curvature in \( \ln \rho(r) \).


import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
import os
import shutil
import pandas as pd

# --- SCG MODEL AND METRICS ---

def scg_velocity(x, A, B):
    """
    SCG rotation velocity model:
    v(r) = A * r^((1 - B)/2)
    This follows directly from SCG's causal-density curvature relation.
    """
    return A * x**((1 - B) / 2)

def rmsd(observed, predicted):
    """
    Computes root mean square deviation (RMSD) between observed and predicted values.
    Used to assess model fit quality.
    """
    return np.sqrt(np.mean((observed - predicted) ** 2))


# --- SINGLE-REGION SCG FIT ---

def fit_region(radius, velocity):
    """
    Fits the SCG model to a region of (radius, velocity) data.
    Returns best-fit parameters, predicted values, and RMSD.
    """
    if len(radius) < 4:
        return None  # Too few data points for a stable fit
    try:
        params, _ = curve_fit(scg_velocity, radius, velocity, p0=[100, 1.7])
        pred = scg_velocity(radius, *params)
        return params, pred, rmsd(velocity, pred)
    except RuntimeError:
        return None  # Fitting failed (e.g., numerical instability)


# --- DUAL-REGION SPLIT FIT SEARCH ---

def find_best_break(radius, velocity, min_gap=5):
    """
    Finds the best radius to split the dataset into two zones,
    minimizing combined RMSD of two SCG fits.
    """
    best_rmsd = float('inf')
    best_split = None
    best_fits = None

    for i in range(min_gap, len(radius) - min_gap):
        r_break = radius[i]
        left_mask = radius < r_break
        right_mask = radius >= r_break

        fit_left = fit_region(radius[left_mask], velocity[left_mask])
        fit_right = fit_region(radius[right_mask], velocity[right_mask])

        if fit_left and fit_right:
            total_rmsd = (
                fit_left[2] * np.sum(left_mask) +
                fit_right[2] * np.sum(right_mask)
            ) / len(radius)

            if total_rmsd < best_rmsd:
                best_rmsd = total_rmsd
                best_split = r_break
                best_fits = (fit_left, fit_right)

    return best_split, best_fits, best_rmsd


# --- PER-FILE (PER-GALAXY) FITTING LOGIC ---

def process_file(filepath, output_dir="graphs_final", processed_dir="processed_final", min_improvement=0.20):
    """
    Processes a single galaxy data file:
    - Performs global (single) SCG fit
    - Optionally applies dual-region split if significantly better
    - Generates a plot
    - Returns summary fit metrics
    """
    galaxy_name = os.path.basename(filepath).split('_')[0]

    with open(filepath, 'r') as file:
        lines = file.readlines()
    data = [line.strip().split() for line in lines if not line.startswith("#")]

    radius = np.array([float(row[0]) for row in data])
    v_obs = np.array([float(row[1]) for row in data])
    num_points = len(radius)

    global_fit = fit_region(radius, v_obs)
    if not global_fit:
        return {
            "Galaxy": galaxy_name,
            "Best Fit": "Failed",
            "Global RMSD": None,
            "Dual RMSD": None,
            "Break Radius": None,
            "A1": None, "B1": None, "A2": None, "B2": None,
        }

    global_rmsd = global_fit[2]
    use_dual = False
    break_radius, split_fits, dual_rmsd = None, (None, None), None

    # Try dual-region fit if there are enough data points
    if num_points >= 10:
        break_radius, split_fits, dual_rmsd = find_best_break(radius, v_obs)
        # Only switch to dual if improvement over single is meaningful
        if break_radius and global_rmsd > 0 and ((global_rmsd - dual_rmsd) / global_rmsd) >= min_improvement:
            use_dual = True

    # --- Visualization ---
    os.makedirs(output_dir, exist_ok=True)
    fig, ax = plt.subplots(figsize=(9, 5))
    ax.plot(radius, v_obs, 'o', label='Observed', markersize=4)

    if use_dual:
        fit_left, fit_right = split_fits
        ax.plot(radius[radius < break_radius], fit_left[1], '-', label=f'Inner Fit B={fit_left[0][1]:.3f}')
        ax.plot(radius[radius >= break_radius], fit_right[1], '-', label=f'Outer Fit B={fit_right[0][1]:.3f}')
        ax.axvline(break_radius, color='gray', linestyle='--', alpha=0.6, label=f'Break @ {break_radius:.2f} kpc')
        title = f'{galaxy_name} | Best Fit: Dual | RMSD: {dual_rmsd:.2f}'
    else:
        ax.plot(radius, global_fit[1], '-', label=f'Single Fit B={global_fit[0][1]:.3f}')
        title = f'{galaxy_name} | Best Fit: Single | RMSD: {global_rmsd:.2f}'

    ax.set_title(title)
    ax.set_xlabel('Radius (kpc)')
    ax.set_ylabel('Velocity (km/s)')
    ax.grid(True)
    ax.legend()
    plt.tight_layout()
    plt.savefig(f"{output_dir}/{galaxy_name}_best_fit.png")
    plt.close()

    # Move file to processed directory
    os.makedirs(processed_dir, exist_ok=True)
    shutil.move(filepath, os.path.join(processed_dir, os.path.basename(filepath)))
    print(filepath)

    return {
        "Galaxy": galaxy_name,
        "Datapoints": num_points,
        "Best Fit": "Dual" if use_dual else "Single",
        "Global RMSD": global_rmsd,
        "Dual RMSD": dual_rmsd if use_dual else None,
        "Break Radius": break_radius if use_dual else None,
        "A1": split_fits[0][0][0] if use_dual else global_fit[0][0],
        "B1": split_fits[0][0][1] if use_dual else global_fit[0][1],
        "A2": split_fits[1][0][0] if use_dual else None,
        "B2": split_fits[1][0][1] if use_dual else None,
    }


# --- BATCH PROCESSING LOOP ---

def batch_process():
    """
    Runs SCG fitting on all galaxy data files in the current directory
    (expects filenames ending with '_rotmod.dat').
    Outputs results to CSV.
    """
    results = []
    for filename in os.listdir():
        if filename.endswith("_rotmod.dat"):
            result = process_file(filename)
            if result:
                results.append(result)

    df = pd.DataFrame(results)
    df.to_csv("SCG_best_fit_summary.csv", index=False)
    print("✅ SCG model selection complete. Summary saved to SCG_best_fit_summary.csv.")


# --- ENTRY POINT ---

if __name__ == "__main__":
    batch_process()

References and Source Links

For updates, examples, or implementation help, contact D. J. Hallman SCG@azfn.com.