<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Svarbhanu Neel</title>
    <description>The latest articles on DEV Community by Svarbhanu Neel (@svarbhanu).</description>
    <link>https://dev.to/svarbhanu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015079%2Fdbe168ca-5f2b-492f-857b-b60eea44dd50.jpg</url>
      <title>DEV Community: Svarbhanu Neel</title>
      <link>https://dev.to/svarbhanu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/svarbhanu"/>
    <language>en</language>
    <item>
      <title>I built a zero-dependency sky engine in TypeScript, verified to 4.6 arcseconds</title>
      <dc:creator>Svarbhanu Neel</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:21:55 +0000</pubDate>
      <link>https://dev.to/svarbhanu/i-built-a-zero-dependency-sky-engine-in-typescript-verified-to-46-arcseconds-317h</link>
      <guid>https://dev.to/svarbhanu/i-built-a-zero-dependency-sky-engine-in-typescript-verified-to-46-arcseconds-317h</guid>
      <description>&lt;p&gt;Today I shipped v0.1.0 of &lt;a href="https://github.com/svarbhanu/grahan" rel="noopener noreferrer"&gt;grahan&lt;/a&gt;, an MIT-licensed TypeScript library that computes Sun/Moon positions, sunrise/sunset, moon phases, and the Vedic panchang (tithi, nakshatra, yoga, karana, Rahu Kaal). Zero runtime dependencies. Runs in Node 18+, browsers, and edge runtimes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @grahan/vedic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;panchang&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@grahan/vedic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;panchang&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-07-02T06:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;27.7172&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;85.324&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Asia/Kathmandu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// p.tithi      → { index: 17, paksha: 'krishna', name: 'Tritiya' }&lt;/span&gt;
&lt;span class="c1"&gt;// p.nakshatra  → { index: 21, name: 'Shravana', pada: 1 }&lt;/span&gt;
&lt;span class="c1"&gt;// p.rahuKaal   → 13:51 to 15:35 local, that Thursday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post is about the three engineering decisions that mattered, and the bugs the process caught that I would never have found by eyeballing outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why zero dependencies
&lt;/h2&gt;

&lt;p&gt;The industry-standard ephemeris is Swiss Ephemeris. It is superb, but AGPL (or paid), so linking it was off the table for an MIT library. The alternatives were: wrap a WASM build (license problem remains), depend on a chain of npm astronomy packages of varying maintenance, or implement from the public-domain sources: Meeus's &lt;em&gt;Astronomical Algorithms&lt;/em&gt;, the VSOP87 planetary theory, and the ELP-2000 lunar series.&lt;/p&gt;

&lt;p&gt;I implemented from sources. It also bought me something I didn't expect to value so much: the whole library is auditable. Every formula has a citation to a chapter of Meeus or an official data file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule one: generate the answer sheet before writing the formula
&lt;/h2&gt;

&lt;p&gt;Swiss Ephemeris is AGPL &lt;em&gt;code&lt;/em&gt;, but its &lt;em&gt;outputs&lt;/em&gt; are facts. So before writing any calculation, I ran an offline Python script (pyswisseph) that generated JSON reference fixtures and committed them to the repo: 120 Sun/Moon longitudes spanning 1900 to 2100, 150 sunrise/sunset events across 5 sites from Singapore to Utqiagvik at 71°N, 41 ayanamsa epochs. Tests read the JSON and assert tolerances, and CI prints an accuracy report on every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[accuracy] sun apparent longitude: n=120 max=4.60″ mean=0.85″
[accuracy] moon apparent longitude: n=120 max=65.37″ mean=10.46″
[accuracy] sunrise/sunset: n=272 max=4.6s mean=0.9s polar-states=28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Promised tolerances are ±36″ (Sun), ±180″ (Moon), ±60 s (rise/set). Measured is 8 to 13 times inside the promise. The README publishes both columns, promise and measurement, because accuracy claims without numbers are marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule two: don't type coefficient tables by hand
&lt;/h2&gt;

&lt;p&gt;The Sun's position needs a truncated VSOP87 series: hundreds of coefficients. Typing them from a book is how you get a library that's subtly wrong forever. Instead, a script downloads the official VSOP87D data file from the CDS astronomical data center, truncates it by amplitude, and emits the TypeScript table with provenance in the header. The Moon's 120-term table I did have to type from Meeus, but the fixtures had my back: the mean error came out at 10.5″, exactly the intrinsic accuracy Meeus states for that truncation. A single typo would have inflated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs the fixtures caught
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The nutation fingerprint.&lt;/strong&gt; My lunar-node function disagreed with the reference by a systematic 11 to 18 arcseconds. The &lt;em&gt;scale&lt;/em&gt; was the clue: nutation oscillates ±17.2″. Swiss Ephemeris reports the node on the true equinox of date; my polynomial gave the mean equinox. One line fixed it, and max error fell to 0.35″. Good reference data doesn't just say &lt;em&gt;wrong&lt;/em&gt;. The error's shape tells you &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sunset before sunrise.&lt;/strong&gt; At 71°N, near the midnight-sun transition, a local calendar day can contain a sunset at 01:30 and a sunrise at 02:40, in that order. My test asserted &lt;code&gt;sunrise &amp;lt; sunset&lt;/code&gt; as a universal property. The fixture data taught me otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The neighbouring solar cycle.&lt;/strong&gt; Also at 71°N: a local day whose sunset belongs to the &lt;em&gt;previous&lt;/em&gt; day's solar arc, 15 minutes after local midnight. My first algorithm only searched around that day's own solar noon and reported "always up." The fix: consider events from adjacent transits and keep whatever lands inside the local day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The refraction calibration.&lt;/strong&gt; First full run: every site showed sunrise late and sunset early by the same amount. The day was symmetrically too short, and that signature means exactly one thing: the horizon-altitude constant is wrong. The textbook says 34′ of refraction; Swiss Ephemeris effectively bends about 36.7′ at the event's true altitude. At tangent polar crossings that 0.6′ costs two minutes. One calibrated constant, documented with its provenance, took max error from 115 s to 4.6 s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The empty tarball.&lt;/strong&gt; My npm build hook was &lt;code&gt;prepublishOnly&lt;/code&gt;, which does not run on &lt;code&gt;pack&lt;/code&gt;. CI packed tarballs with no compiled code in them. I only caught it because the CI installs the packed tarballs with plain npm into a scratch project on Node 18/20/22 and runs a smoke test, exactly as a consumer would. If your CI only ever tests your source tree, you are not testing the thing you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: secular core, cultural layers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@grahan/core&lt;/code&gt; is pure astronomy. A weather or photography app can use it without touching a single Vedic concept. &lt;code&gt;@grahan/vedic&lt;/code&gt; layers the cultural interpretation on top: the Lahiri ayanamsa (fitted to reference values to 0.002″), then tithi, nakshatra, yoga, and karana as pure functions over longitudes. Planned layers on the same core: world calendars (Bikram Sambat, Hijri, Hebrew), prayer times, tropical charts.&lt;/p&gt;

&lt;p&gt;A detail I enjoyed: the ayanamsa &lt;em&gt;cancels&lt;/em&gt; in the Moon minus Sun difference, so tithi works on tropical longitudes directly. But it doesn't cancel in the Moon plus Sun &lt;em&gt;sum&lt;/em&gt;, so yoga genuinely needs the sidereal frame. The kind of thing you only internalize by implementing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Phase 2 is &lt;code&gt;kundali()&lt;/code&gt;, full birth charts: the remaining planets, lagna, whole-sign houses, D9, and SVG chart renderers. Same method: fixtures first, formulas second.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/svarbhanu/grahan" rel="noopener noreferrer"&gt;https://github.com/svarbhanu/grahan&lt;/a&gt; · Packages: &lt;a href="https://www.npmjs.com/package/@grahan/core" rel="noopener noreferrer"&gt;@grahan/core&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@grahan/vedic" rel="noopener noreferrer"&gt;@grahan/vedic&lt;/a&gt;. Issues and PRs welcome, especially from people who know panchang traditions better than I do.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>astrnomy</category>
    </item>
  </channel>
</rss>
