সূর্য সিদ্ধান্ত পঞ্জিকা সকল প্রবন্ধ

রাশি ও নক্ষত্রের সীমানা: Rounding, Precision ও Boundary Test কীভাবে করবেন?

০°/৩৬০° wrap, রাশি, নক্ষত্র, তিথি, করণ ও যোগের boundary classification, astronomical root-finding, DMS carry এবং C# regression test-এর পূর্ণ নির্দেশিকা।

পঞ্চাঙ্গের অধিকাংশ নাম একটি continuous angular quantity-কে সমান segment-এ ভাগ করে পাওয়া যায়। Angle সীমানার সামান্য আগে না পরে আছে—এই সিদ্ধান্তেই বর্তমান রাশি, নক্ষত্র, তিথি, করণ বা যোগ নির্ধারিত হয়। অতএব boundary handling শুধু formatting-এর বিষয় নয়; এটি calendar logic-এর অংশ।

Boundary কেন এত গুরুত্বপূর্ণ?

একটি longitude ২৯.৯৯৯৯৯৯° হলে তা এখনও প্রথম রাশির মধ্যে। ৩০.০০০০০০° হলে পরবর্তী রাশি শুরু। যদি প্রথম value-কে display-এর জন্য ৩০.০০° round করে তারপর classification চালানো হয়, transition বাস্তবের আগেই ঘটে গেছে বলে ধরে নেওয়া হবে।

একই সমস্যা আরও কয়েকভাবে দেখা যায়:

  • Moon ১৩°২০′ অতিক্রম করলে নক্ষত্র বদলায়;
  • Moon–Sun elongation ১২° অতিক্রম করলে তিথি বদলায়;
  • Elongation-এর প্রতি ৬°-এ করণ segment বদলায়;
  • Nirāyaṇa Sun+Moon sum-এর প্রতি ১৩°২০′-এ যোগ বদলায়;
  • Sun ৩৬০° থেকে ০°-এ গেলে Meṣa boundary crossing হয়;
  • Event time সূর্যোদয় বা মধ্যরাত্রির কয়েক second এদিক-ওদিক হলে প্রয়োগের দিন বদলাতে পারে।

Panchanga segment widths

উপাদান মূল angle Segment সংখ্যা প্রতি segment
রাশি Nirāyaṇa longitude ১২ ৩০°
নক্ষত্র Nirāyaṇa Moon ২৭ ১৩°২০′
তিথি Normalize(Moon − Sun) ৩০ ১২°
করণ half-segment Normalize(Moon − Sun) ৬০ ৬°
যোগ Normalize(Nirāyaṇa Sun + Moon) ২৭ ১৩°২০′
নবাংশ—যদি ব্যবহৃত হয় Nirāyaṇa longitude ১০৮ ৩°২০′

Segment width দশমিক literal হিসেবে লিখলে ১৩.৩৩৩৩ বা ৩.৩৩৩৩ দিয়ে truncate করবেন না। 360.0 / 27.0 বা scaled-index formula ব্যবহার করুন, যাতে source definition অক্ষুণ্ণ থাকে।

০°/৩৬০° normalization

C# remainder operator negative value-কে positive circle-এ নিজে থেকে ফিরিয়ে দেয় না। তাই একটি robust normalization function দরকার:

static double Normalize360(double value)
{
    if (Double.IsNaN(value) || Double.IsInfinity(value))
        throw new ArgumentOutOfRangeException("value");

    double normalized = value % 360.0;
    if (normalized < 0.0) normalized += 360.0;

    // Protect the half-open range if upstream produces 360 by rounding.
    if (normalized >= 360.0) normalized = 0.0;
    return normalized;
}
Input Normalized
−১°৩৫৯°
০°০°
৩৬০°০°
৩৬১°১°
৭২০°০°

Shortest signed angular difference

static double SignedAngle(double value, double target)
{
    double difference = Normalize360(value - target);
    if (difference >= 180.0) difference -= 360.0;
    return difference;
}

৩৫৯.৯° থেকে ০.১°-এর forward separation ০.২°; ordinary subtraction −৩৫৯.৮°। Root search, comparison ও transit direction-এ wrapped angular difference ব্যবহার না করলে ০° boundary-তে false jump তৈরি হয়।

Half-open interval policy: [start, end)

প্রতিটি segment-কে inclusive start এবং exclusive end হিসেবে সংজ্ঞায়িত করুন:

Segment 0 = [0°, width)

Segment 1 = [width, 2 × width)

…

Exact boundary নতুন segment-এর অন্তর্গত

এই policy-তে ৩০° ঠিক হলে বৃষ শুরু, ১৩°২০′ ঠিক হলে দ্বিতীয় নক্ষত্র শুরু এবং elongation ১২° ঠিক হলে দ্বিতীয়া শুরু। Half-open interval ব্যবহার করলে কোনো exact angle দুটি segment-এ পড়ে না এবং কোনো gap থাকে না।

Calendar nuance: Exact astronomical boundary-তে নতুন angular segment শুরু হলেও কোন পঞ্জিকা-দিনে সেই নতুন নাম প্রয়োগ হবে তা sunrise, midnight বা festival rule আলাদাভাবে নির্ধারণ করে।

Floating-point বাস্তবতা

.NET double একটি binary floating-point type, সাধারণত প্রায় ১৫–১৭ decimal digit precision দেয়। ০.১, ১৩⅓ বা বহু astronomical fraction binary-তে exact নয়। ফলে mathematically exact boundary code-এ এমন হতে পারে:

29.999999999999996
30.000000000000004

এটি library failure নয়; floating representation-এর স্বাভাবিক বৈশিষ্ট্য। সমাধান হলো arbitrary round করে value বদলে দেওয়া নয়, বরং calculation contract স্পষ্ট করা।

Double.Epsilon comparison tolerance নয়

Double.Epsilon হলো zero-এর ওপরে ক্ষুদ্রতম positive representable double-এর কাছাকাছি একটি value; astronomical longitude-এর practical error tolerance নয়। Ephemeris uncertainty, solver tolerance ও quantity scale অনুযায়ী tolerance নির্ধারণ করুন।

একটি universal epsilon-ও নিরাপদ নয়

সব angle-এ +1e-8 যোগ করে Floor করলে boundary আগেভাগে পার হয়ে যাবে। আবার epsilon খুব ছোট হলে কোনো উপকার হবে না। তাই:

  1. Ordinary instant classification raw normalized angle দিয়ে করুন;
  2. Exact transition দরকার হলে root solve করুন;
  3. Near-boundary diagnostics-এ explicit uncertainty state দিন;
  4. Display rounding কখনও classifier input হবে না।

C# segment classification

static int SegmentIndex(double angle, int segmentCount)
{
    if (segmentCount <= 0)
        throw new ArgumentOutOfRangeException("segmentCount");

    double normalized = Normalize360(angle);
    int index = (int)Math.Floor(
        normalized * segmentCount / 360.0);

    // Defensive only; Normalize360 promises [0, 360).
    if (index < 0) index = 0;
    if (index >= segmentCount) index = segmentCount - 1;
    return index;
}

static int RashiIndex(double nirayanaLongitude)
{
    return SegmentIndex(nirayanaLongitude, 12);
}

static int NakshatraIndex(double nirayanaMoon)
{
    return SegmentIndex(nirayanaMoon, 27);
}

static int TithiNumber(double moonLongitude, double sunLongitude)
{
    double elongation = Normalize360(moonLongitude - sunLongitude);
    return SegmentIndex(elongation, 30) + 1;
}

static int KaranaHalfIndex(double moonLongitude, double sunLongitude)
{
    double elongation = Normalize360(moonLongitude - sunLongitude);
    return SegmentIndex(elongation, 60);
}

static int YogaIndex(double nirayanaSun, double nirayanaMoon)
{
    return SegmentIndex(nirayanaSun + nirayanaMoon, 27);
}

Karana half-index থেকে নাম নির্ধারণের সময় repeating seven karaṇa এবং four fixed karaṇa-র traditional sequence আলাদা rule হিসেবে প্রয়োগ করতে হবে। Index formula ঠিক হলেই karana name mapping স্বয়ংক্রিয়ভাবে ঠিক হয় না।

Near-boundary diagnostic

public sealed class BoundaryDiagnostic
{
    public int SegmentIndex { get; set; }
    public double NormalizedAngle { get; set; }
    public double PreviousBoundary { get; set; }
    public double NextBoundary { get; set; }
    public double DistanceToNearestBoundary { get; set; }
    public bool IsWithinDiagnosticTolerance { get; set; }
}

static BoundaryDiagnostic InspectBoundary(
    double angle,
    int segmentCount,
    double diagnosticToleranceDegrees)
{
    double value = Normalize360(angle);
    int index = SegmentIndex(value, segmentCount);
    double width = 360.0 / segmentCount;
    double previous = index * width;
    double next = (index + 1) * width;
    double fromPrevious = value - previous;
    double toNext = next - value;
    double nearest = Math.Min(fromPrevious, toNext);

    BoundaryDiagnostic result = new BoundaryDiagnostic();
    result.SegmentIndex = index;
    result.NormalizedAngle = value;
    result.PreviousBoundary = Normalize360(previous);
    result.NextBoundary = Normalize360(next);
    result.DistanceToNearestBoundary = nearest;
    result.IsWithinDiagnosticTolerance =
        nearest <= diagnosticToleranceDegrees;
    return result;
}

এই diagnostic classifier-এর সিদ্ধান্ত বদলায় না; শুধু report-এ জানায় যে result boundary-এর খুব কাছে এবং displayed rounded value দেখে segment বিচার করা উচিত নয়।

Transition instant: equality নয়, root-finding

“প্রতি মিনিটে longitude বের করে প্রথম ৩০.০০ পাওয়া” reliable algorithm নয়। Event function তৈরি করে এমন দুটি JD খুঁজুন যেখানে function-এর sign বদলায়; তারপর bisection বা Brent-type solver দিয়ে root refine করুন।

Boundary event function

static double BoundaryFunction(
    Func<double, double> longitudeAtJulianDay,
    double targetLongitude,
    double julianDay)
{
    double longitude = longitudeAtJulianDay(julianDay);
    return SignedAngle(longitude, targetLongitude);
}

Bracket ছোট রাখুন, যাতে target-এর বিপরীত বিন্দু—target ±180°—এর wrap-কে false sign change মনে না হয়। সূর্য, চন্দ্র ও retrograde planet-এর জন্য search step এক হবে না। Retrograde body একই boundary বহুবার পার হতে পারে; direction metadata রাখুন।

C# 5-compatible bisection

static double BisectRoot(
    Func<double, double> function,
    double left,
    double right,
    double timeToleranceDays,
    int maxIterations)
{
    double fLeft = function(left);
    double fRight = function(right);

    if (Double.IsNaN(fLeft) || Double.IsNaN(fRight))
        throw new InvalidOperationException("Boundary function returned NaN.");

    if (fLeft == 0.0) return left;
    if (fRight == 0.0) return right;
    if (Math.Sign(fLeft) == Math.Sign(fRight))
        throw new ArgumentException("Root is not bracketed.");

    for (int i = 0; i < maxIterations; i++)
    {
        double middle = left + (right - left) / 2.0;
        double fMiddle = function(middle);

        if (Math.Abs(right - left) <= timeToleranceDays)
            return middle;

        if (fMiddle == 0.0) return middle;

        if (Math.Sign(fLeft) != Math.Sign(fMiddle))
        {
            right = middle;
            fRight = fMiddle;
        }
        else
        {
            left = middle;
            fLeft = fMiddle;
        }
    }

    throw new InvalidOperationException(
        "Boundary root did not converge within the iteration limit.");
}

এক second time tolerance চাইলে:

timeToleranceDays = 1 ÷ 86400

Angular tolerance ও time tolerance এক নয়। সূর্য প্রায় ১°/day চললে ১ arcsecond longitude প্রায় ২৪ seconds time-এর সমান; Moon-এর দ্রুত গতিতে একই ১ arcsecond অনেক কম সময়। তাই body rate না দেখে angle tolerance-কে clock tolerance বলা যাবে না।

Display rounding ও DMS carry

Calculation-এর raw longitude রেখে আলাদা formatter ব্যবহার করুন। Second round হয়ে 60 হলে minute-এ carry, minute 60 হলে degree-তে carry এবং 30° হলে rāśi-তে carry লাগতে পারে।

public sealed class DmsValue
{
    public int Degree { get; set; }
    public int Minute { get; set; }
    public int Second { get; set; }
}

static DmsValue ToDmsInsideSegment(
    double angleInsideSegment,
    double segmentWidth)
{
    double safe = angleInsideSegment;
    if (safe < 0.0) safe = 0.0;

    long totalSeconds = (long)Math.Round(
        safe * 3600.0,
        MidpointRounding.AwayFromZero);

    long maximum = (long)Math.Round(
        segmentWidth * 3600.0,
        MidpointRounding.AwayFromZero);

    // Formatting must not silently turn the previous segment into the next.
    if (totalSeconds >= maximum)
        totalSeconds = maximum - 1;

    DmsValue result = new DmsValue();
    result.Degree = (int)(totalSeconds / 3600L);
    result.Minute = (int)((totalSeconds % 3600L) / 60L);
    result.Second = (int)(totalSeconds % 60L);
    return result;
}

উপরের clamp শুধু “current segment-এর ভিতরের display” বানাতে উপযোগী। Astronomical longitude-এর সাধারণ rounded display-এ carry করে পরবর্তী rāśi label দেখানোও বৈধ হতে পারে—তবে classification raw value থেকেই থাকবে। UI-তে চাইলে ২৯°৫৯′৫৯″+ বা “boundary-এর নিকট” note দেখান।

Rounding mode প্রকাশ করুন

.NET-এর Math.Round overload ও MidpointRounding mode অনুযায়ী midpoint আচরণ বদলায়। Report snapshot স্থিতিশীল রাখতে formatter-এ mode explicit করুন; environment default-এর ওপর নির্ভর করবেন না।

Calendar rule-এ formatted time নয়, raw instant

সংক্রান্তি ১১:৫৯:৫৯.৬ PM হলে display “১২:০০:০০ AM” হতে পারে। যদি month-start rule formatted clock string parse করে, ঘটনা ভুল দিনে পড়বে। Rule engine-এ ব্যবহার করুন:

  • Raw Julian Day অথবা high-resolution UTC instant;
  • Unrounded local DateTime;
  • Actual sunrise, midnight ও next-sunrise boundaries;
  • Explicit comparison—<, <=, >;
  • Applied boundary policy trace।
bool beforeMidnight = sankrantiLocal < localMidnight;
bool atMidnight = sankrantiLocal == localMidnight;

// Format only after the calendar decision is complete.
string display = sankrantiLocal.ToString(
    "dd MMM yyyy, hh:mm:ss tt",
    CultureInfo.InvariantCulture);

DateTime-এর tick precision থাকলেও upstream ephemeris uncertainty তার চেয়ে বড় হতে পারে। অতএব exact equality বিরল এবং model-dependent; equality policy documented রাখুন। JD-based solver result থেকে DateTime conversion করার সময় early historical range ও calendar conversion আলাদা রাখুন।

Boundary regression tests

প্রতিটি boundary-র তিনটি test

Boundary B এবং small test delta δ নিয়ে পরীক্ষা করুন:

B − δ → previous segment

B → new segment

B + δ → new segment

const double Delta = 1e-10;

static void AssertBoundary(
    double boundary,
    int segmentCount,
    int previousIndex,
    int newIndex)
{
    Assert.AreEqual(previousIndex,
        SegmentIndex(boundary - Delta, segmentCount));
    Assert.AreEqual(newIndex,
        SegmentIndex(boundary, segmentCount));
    Assert.AreEqual(newIndex,
        SegmentIndex(boundary + Delta, segmentCount));
}

Test delta production tolerance নয়; এটি deterministic unit-test probe। Boundary value code-এ একই formula থেকে তৈরি করুন, decimal text copy করে নয়।

প্রস্তাবিত validation checklist

  1. Normalize360(-1) = 359;
  2. Normalize360(0) = 0;
  3. Normalize360(360) = 0;
  4. Normalize360(720) = 0;
  5. NaN rejected;
  6. Infinity rejected;
  7. Segment interval is [start, end);
  8. Exact 30° enters next rāśi;
  9. Every ১২ rāśi boundary tested;
  10. Last rāśi wraps to Meṣa;
  11. Exact 13°20′ enters next nakṣatra;
  12. Every ২৭ nakṣatra boundary tested;
  13. Last nakṣatra wraps to first;
  14. Exact 12° enters next tithi;
  15. Every ৩০ tithi boundary tested;
  16. Amāvasyā end wraps to Śukla Pratipad;
  17. Exact 6° enters next karana half;
  18. Every ৬০ karana half-boundary tested;
  19. Karana name-cycle separately tested;
  20. Exact yoga boundary enters next yoga;
  21. Every ২৭ yoga boundary tested;
  22. Nirāyaṇa profile same across classifier and solver;
  23. Raw longitude retained;
  24. Display longitude stored separately;
  25. No pre-classification rounding;
  26. No universal epsilon added;
  27. Double.Epsilon not used as domain tolerance;
  28. Signed angle at 359.9°/0.1° tested;
  29. Antipodal ±180° behavior tested;
  30. Root bracket sign change required;
  31. Bracket span excludes false antipodal wrap;
  32. Bisection iteration cap enforced;
  33. NaN function result rejected;
  34. Time tolerance expressed in days;
  35. Root returned inside bracket;
  36. Forward crossing direction retained;
  37. Retrograde crossing direction retained;
  38. Multiple crossings not silently collapsed;
  39. DMS second carry tested;
  40. DMS minute carry tested;
  41. Rāśi carry/wrap tested;
  42. Rounding mode explicit;
  43. Calendar rule uses raw instant;
  44. Sunrise equality policy tested;
  45. Midnight equality policy tested;
  46. Next-sunrise equality policy tested;
  47. Historical JD retained through solve;
  48. HTML/XML/API raw values agree;
  49. Invariant-culture serialization tested;
  50. Regression snapshots include engine/profile version।

সাধারণ ভুল ও সমাধান

ভুল ফল সমাধান
Round করে তারপর Floor Boundary আগেভাগে পার Raw angle দিয়ে classification
Negative remainder normalize না করা Negative segment index Normalize360
৩৬০°-কে segment ১২/২৭ ধরা Array index error ৩৬০° → ০°
সব calculation-এ +epsilon Systematic early transition Root solver ও diagnostic tolerance
Double.Epsilon domain tolerance প্রায় কোনো practical effect নেই Model/solver-based tolerance
Minute-by-minute equality search Event miss বা coarse time Bracket + root refinement
Formatted midnight দিয়ে rule দিন/মাস ভুল Raw instant comparison
DMS-তে 60 second print অবৈধ display Carry normalization

উপসংহার

রাশি, নক্ষত্র, তিথি, করণ ও যোগ—সবগুলোই angular boundary classification। সঠিক result-এর জন্য angle প্রথমে [0°, 360°) range-এ normalize করতে হবে, segment-কে [start, end) policy-তে ভাগ করতে হবে এবং unrounded value দিয়ে index নির্ধারণ করতে হবে।

Transition time equality search করে নয়, bracketed root solver দিয়ে বের করুন। Angular tolerance, clock tolerance ও display precision আলাদা parameter। Display-এর DMS বা clock round হবে calculation শেষ হওয়ার পরে; calendar rule সর্বদা raw event instant ব্যবহার করবে।

এই separation বজায় রাখলে ২৯°৫৯′৫৯.৯৯″, ০°/৩৬০° wrap, মধ্যরাত্রির কয়েক fraction-second এবং historical JD—সব boundary case একই consistent policy-তে পরিচালিত হবে।

তথ্যসূত্র ও আরও পাঠ

  1. Microsoft Learn—C# floating-point numeric types; double range ও approximate precision.
  2. Microsoft Learn—Math.Round; .NET Framework overload ও rounding behavior.
  3. Microsoft Learn—MidpointRounding; explicit midpoint strategy.
  4. Microsoft Learn—Math.Floor; segment-index calculation.
  5. Swiss Ephemeris Programmer’s Documentation; calculation interface, transit/crossing tools ও numerical considerations.
  6. Swiss Ephemeris General Documentation; ephemeris precision, coordinate models ও comparison guidance.

মন্তব্য, আলোচনা ও প্রশ্ন