<?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: taurus yao</title>
    <description>The latest articles on DEV Community by taurus yao (@taurusyao).</description>
    <link>https://dev.to/taurusyao</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%2F4164137%2Fa7e13135-3677-4d71-97ce-29b4cf04f531.png</url>
      <title>DEV Community: taurus yao</title>
      <link>https://dev.to/taurusyao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taurusyao"/>
    <language>en</language>
    <item>
      <title>The Opposite Slot Comes Free: Modelling a Paired Reading Instead of a Single Lookup</title>
      <dc:creator>taurus yao</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:31:42 +0000</pubDate>
      <link>https://dev.to/taurusyao/the-opposite-slot-comes-free-modelling-a-paired-reading-instead-of-a-single-lookup-1dha</link>
      <guid>https://dev.to/taurusyao/the-opposite-slot-comes-free-modelling-a-paired-reading-instead-of-a-single-lookup-1dha</guid>
      <description>&lt;h1&gt;
  
  
  The Opposite Slot Comes Free: Modelling a Paired Reading Instead of a Single Lookup
&lt;/h1&gt;

&lt;p&gt;Every rule-heavy domain has the same shape. A small set of conventions gets applied in a fixed order, the output is a structured record, and then a human reads that record and produces prose. The prose gets all the attention. The substrate is where the engineering actually lives.&lt;/p&gt;

&lt;p&gt;I want to use one slot out of a twelve-slot chart as a worked example, because that slot has a property most of the other slots do not: &lt;strong&gt;it is fully determined by another slot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Zi Wei Dou Shu the slot that describes a person's life outside home ground — travel, relocation, unfamiliar places, public exposure — sits directly opposite the slot that describes the person themselves. That single geometric fact is worth more than it looks. It turns a lookup into a comparison, and comparisons are testable in ways that lookups are not.&lt;/p&gt;

&lt;p&gt;Nothing below requires any astrology background. No forecasts, no verdicts. The subject is only the data model.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The slot is a congruence, not an entry in a table
&lt;/h2&gt;

&lt;p&gt;The twelve slots have a fixed label order, identical for every chart. Only the rotation is personal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LABELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Life&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Siblings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Children&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wealth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Travel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Friends&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Career&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fortune&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Parents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LABELS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LABELS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;LABELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Travel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;                                    &lt;span class="c1"&gt;# life at 0 -&amp;gt; travel at 6
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# involution
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;             &lt;span class="c1"&gt;# no fixed points
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties fall out of that, and all three are worth exploiting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One index determines the other.&lt;/strong&gt; Store the rotation once, derive the pair. If the renderer and the reading engine each compute the slot independently, they can disagree, and the disagreement will look like a &lt;em&gt;content&lt;/em&gt; bug when it is an &lt;em&gt;indexing&lt;/em&gt; bug. Deriving both from one number makes that category of bug unrepresentable — the same argument as any single-source-of-truth change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The relation is its own inverse.&lt;/strong&gt; &lt;code&gt;opposite(opposite(x)) == x&lt;/code&gt; is a free property test. It catches off-by-six errors immediately, which is the most common arithmetic mistake in a ring of this size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fixed points.&lt;/strong&gt; No slot is its own mirror. That is not decoration: it means the pair always carries two distinct pieces of data, so a "paired" reading can never silently collapse into a single-record reading.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. What a record actually holds
&lt;/h2&gt;

&lt;p&gt;Four fields per slot, and only one of them is really interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Palace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;                                                  &lt;span class="c1"&gt;# derived, never authored
&lt;/span&gt;    &lt;span class="n"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;                                           &lt;span class="c1"&gt;# zero, one or two main stars
&lt;/span&gt;    &lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# star -&amp;gt; label, not a number
&lt;/span&gt;    &lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;# star -&amp;gt; transformation kind
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The star set is fixed and small — fourteen of them, drawn from a closed vocabulary. A slot may hold zero, one or two. &lt;strong&gt;The empty case is the one that carries design weight&lt;/strong&gt;, because the convention is that an empty slot is read by borrowing the star of the slot opposite it.&lt;/p&gt;

&lt;p&gt;That is the point where the geometry stops being decorative and becomes load-bearing: one slot's emptiness promotes the other slot's content to primary input. If you build the pair relation as a rendering convenience, an empty slot is an edge case. If you build it as part of the model, an empty slot is just a resolution rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;effective_stars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;palace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A slot with no star borrows the opposite slot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s star. Do not recurse.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;palace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;palace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stars&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;palace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="n"&gt;stars&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment is the whole lesson. If both slots in a pair are empty (it happens), a naive borrow recurses and you either loop forever or invent a star to break the cycle. One level of borrow, then stop, and let the caller see an empty result — the empty result is real information, not a missing value.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Brightness is a sign, not an adjective
&lt;/h2&gt;

&lt;p&gt;Here is the modelling decision that matters most, and the one most implementations get wrong.&lt;/p&gt;

&lt;p&gt;Each main star in each slot carries a brightness rank. Rank is not a qualifier that makes the sentence "nicer". It flips polarity. The same star in the same slot describes outward flourish at the top of the scale and drain at the bottom. Model it as an adjective and your prose templates will contradict each other; model it as a sign and the contradiction becomes a branch you can test.&lt;/p&gt;

&lt;p&gt;The trap: &lt;strong&gt;published tables disagree on how many rungs the scale has&lt;/strong&gt; — five in some, six or seven in others. So do not store an integer. Store the label, and map label to band through a versioned table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A modelling choice, not a transcription of any one published table.
# Different tables use different rung counts, which is exactly why the
# integer must live in config and never in a stored record.
&lt;/span&gt;&lt;span class="n"&gt;BRIGHTNESS_BANDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brightest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bright&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;favourable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strained&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;darkest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;polarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BRIGHTNESS_BANDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# KeyError is the desired failure
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two deliberate choices in five lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A missing label raises instead of defaulting to zero.&lt;/strong&gt; A silent &lt;code&gt;0&lt;/code&gt; would render as a neutral reading and nobody would ever notice the record was incomplete. Fail closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The table is a parameter with a name.&lt;/strong&gt; When you upgrade to a table with a different rung count, the migration is a diff of a config file, not a scan of stored records hunting for integers that used to mean something else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. The pair read is two projections of one record
&lt;/h2&gt;

&lt;p&gt;Given the slot and its mirror, the useful output is not one sentence but two, produced from the same fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;life_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;life_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;life_index&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LABELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;effective_stars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;polarity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;polarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                         &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;effective_stars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inside&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of returning both is that the prose layer is forced to talk about the &lt;em&gt;relationship&lt;/em&gt; — which side is heavier, which side is carrying the borrowed star — rather than emitting a verdict about a single slot. Structural constraint producing better prose, which is usually the opposite of how these systems get built.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Time layers are a cascade, not a merge
&lt;/h2&gt;

&lt;p&gt;A chart is read at three time scales: natal, ten-year period, single year. Activation markers (the transformation set) can appear at any of the three, in the same slot. Two questions have to be answered explicitly, and published conventions do not agree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When an inner layer marks a star that an outer layer already marked, does it &lt;strong&gt;replace&lt;/strong&gt; or &lt;strong&gt;coexist&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Do you read the &lt;strong&gt;union&lt;/strong&gt; of all marks, or the &lt;strong&gt;most specific&lt;/strong&gt; one?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not resolve this inside a rendering function. Keep the ambiguity in the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LAYERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;natal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;marks_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LAYERS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;layer&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;star&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;star&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning a list rather than a scalar means the precedence question stays visible to whoever writes the prose, and stays testable. If you fold it to one value early, you have made a convention decision in a place where nobody will look for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Three tiers, and only the first one is really yours
&lt;/h2&gt;

&lt;p&gt;The most useful thing I can say about this domain is a triage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Slot indices, the pair relation, the involution&lt;/td&gt;
&lt;td&gt;Arithmetic. Assert it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Star placement, brightness labels, transformation assignment&lt;/td&gt;
&lt;td&gt;A table lookup. Only as good as the named table version.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The sentences&lt;/td&gt;
&lt;td&gt;Interpretation. Not checkable.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tier 1 is a handful of lines and a handful of assertions, as above. It is deterministic, fast, and it either passes or it does not.&lt;/p&gt;

&lt;p&gt;Tier 2 is checkable &lt;em&gt;against a version&lt;/em&gt; — which is the only meaningful sense of "checkable" for reference data. The numbers cannot be better than the table they came from, so the table gets a name and a hash, and upgrades get diffed.&lt;/p&gt;

&lt;p&gt;Tier 3 is not checkable, and pretending otherwise is how these products rot. Whatever you conclude about whether a top-rank star in the outer slot means recognition or burnout is a judgement, in the same way that any interpretation of a structured record is a judgement.&lt;/p&gt;

&lt;p&gt;The consequence is a rule: &lt;strong&gt;tier 3 must never back-propagate into tiers 1 and 2.&lt;/strong&gt; If a bad sentence gets "fixed" by editing a reference table, an arithmetic error has been laundered into a data file and you have lost the ability to diff it.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Why this is worth the afternoon
&lt;/h2&gt;

&lt;p&gt;Rule-heavy products fail quietly in one specific way: the deterministic layer drifts, and the prose layer keeps producing confident paragraphs on top of it. A prose error is loud — the sentence is nonsense, someone notices. An arithmetic error is silent, because the output is still fluent.&lt;/p&gt;

&lt;p&gt;A small assertion suite on the structural layer is the cheapest instrument available. It does not make the readings better. It makes the machinery honest, which is the part that can actually be verified.&lt;/p&gt;

&lt;p&gt;The narrow claim of this post, restated: within this chart, the outer slot is an index congruence off the inner slot, an empty slot resolves by one non-recursive borrow, brightness is an ordinal whose polarity band lives in versioned config, and time layers stay un-merged on purpose. That is checkable. The rest is reading.&lt;/p&gt;

&lt;p&gt;If you want to see the same structure rendered from real birth data rather than from my toy dicts, the chart generator is here: &lt;a href="https://askziwei.com/?utm_source=devto" rel="noopener noreferrer"&gt;https://askziwei.com/?utm_source=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>math</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Palace Ring Arithmetic: Why Every Zi Wei Dou Shu Reading Is a Four-Node Query</title>
      <dc:creator>taurus yao</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:31:52 +0000</pubDate>
      <link>https://dev.to/taurusyao/palace-ring-arithmetic-why-every-zi-wei-dou-shu-reading-is-a-four-node-query-2el5</link>
      <guid>https://dev.to/taurusyao/palace-ring-arithmetic-why-every-zi-wei-dou-shu-reading-is-a-four-node-query-2el5</guid>
      <description>&lt;p&gt;Most write-ups about Zi Wei Dou Shu treat it as an interpretive art, and the interpretation layer is genuinely that. But underneath the interpretation sits something much more boring and much more checkable: a twelve-slot ring, a fixed label order, and a small set of table lookups. If you are building anything in a rule-heavy domain — or if you just want to know whether two charts agree for structural rather than stylistic reasons — it pays to look at the substrate first.&lt;/p&gt;

&lt;p&gt;This post walks through the ring as a data model. No astrology background needed. The claim is narrow: &lt;strong&gt;the palace layer is pure index arithmetic plus joins, and that is the part you can verify&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ring is fixed; only its rotation is personal
&lt;/h2&gt;

&lt;p&gt;A chart has exactly twelve sectors. Their labels are not chosen per person. The label order is the same for every chart in existence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0  Life
1  Siblings
2  Spouse
3  Children
4  Wealth
5  Health
6  Travel
7  Friends
8  Career
9  Property
10 Fortune
11 Parents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What varies between people is where the ring gets &lt;em&gt;pinned&lt;/em&gt;. The Life Palace lands on a sector determined by birth month and hour; from there, the other eleven labels follow in the order above, walking the ring in one fixed direction.&lt;/p&gt;

&lt;p&gt;So if &lt;code&gt;L&lt;/code&gt; is the sector index that carries the Life Palace, every other label is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Life&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Siblings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Spouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Children&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wealth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Travel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Friends&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Career&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fortune&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Parents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;palace_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;life_sector&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ORDER&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;sector&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;life_sector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve slots, one offset, twelve answers. Nothing here depends on the person, the year, the stars, or the reading. Two implementations that disagree about which label sits on which sector differ on &lt;code&gt;life_sector&lt;/code&gt; (or on the direction of travel), and you can prove which one is wrong by drawing twelve boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six opposite pairs fall out of the arithmetic
&lt;/h2&gt;

&lt;p&gt;Because twelve slots pair up across the centre, every palace has an opposite at &lt;code&gt;+6&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;offset&lt;/th&gt;
&lt;th&gt;pairs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 &amp;lt;-&amp;gt; 6&lt;/td&gt;
&lt;td&gt;Life / Travel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 &amp;lt;-&amp;gt; 7&lt;/td&gt;
&lt;td&gt;Siblings / Friends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 &amp;lt;-&amp;gt; 8&lt;/td&gt;
&lt;td&gt;Spouse / Career&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 &amp;lt;-&amp;gt; 9&lt;/td&gt;
&lt;td&gt;Children / Property&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 &amp;lt;-&amp;gt; 10&lt;/td&gt;
&lt;td&gt;Wealth / Fortune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 &amp;lt;-&amp;gt; 11&lt;/td&gt;
&lt;td&gt;Health / Parents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that this list is &lt;em&gt;derived&lt;/em&gt;, not memorised. Once the label order and the &lt;code&gt;+6&lt;/code&gt; rule are fixed, the six pairs are the only possible output. The interpretive claim attached to each pair — that career reads against partnership, that money reads against how content you feel — is a separate layer sitting on top of the geometry. Keeping those two layers apart is the difference between a system you can test and a system you can only argue about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every reading is a four-node query
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprises people who assume a "reading" means one box. In practice a palace is never judged alone. It is read together with two more palaces at &lt;code&gt;-4&lt;/code&gt; and &lt;code&gt;+4&lt;/code&gt;, plus the opposite at &lt;code&gt;+6&lt;/code&gt;. That four-slot set is the standard unit of analysis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reading_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sector&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;({(&lt;/span&gt;&lt;span class="n"&gt;sector&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it for the Life Palace at sector 0: you get &lt;code&gt;{0, 4, 6, 8}&lt;/code&gt; — Life, Wealth, Travel, Career. Run it for Travel at sector 6: &lt;code&gt;{0, 2, 6, 10}&lt;/code&gt; → Life, Spouse, Travel, Fortune.&lt;/p&gt;

&lt;p&gt;Both sets contain the same two axis nodes and nothing else in common. A palace and its opposite are &lt;em&gt;linked&lt;/em&gt; but not &lt;em&gt;equivalent&lt;/em&gt;: they share the axis, while each brings its own &lt;code&gt;±4&lt;/code&gt; pair. That is the exact sense in which the pair reads as one topic seen from two directions — the shared part is the conflict, the unshared part is what each side adds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;opp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;opp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reading_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                   &lt;span class="c1"&gt;# opposite is in the set
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reading_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;reading_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;           &lt;span class="c1"&gt;# trine pairs are disjoint
&lt;/span&gt;    &lt;span class="nf"&gt;assert &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;                                     &lt;span class="c1"&gt;# the relation inverts itself
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three one-line assertions, and you have pinned down the entire read-time topology. If your implementation of "the three-parties-and-four-positions" rule fails any of them, it is wrong, and it is wrong in a way you can point at.&lt;/p&gt;

&lt;p&gt;The practical consequence: a reading is a &lt;strong&gt;query over four nodes&lt;/strong&gt;, not a property of one node. That is why two readers can look at the same chart and emphasise different things without contradicting each other — they may be weighting different nodes in the same four-element set. It also means a cached interpretation keyed on a single palace is under-keyed. Key it on the four-slot set plus the star set inside those slots, and the cache stops returning stale text when a neighbouring palace changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the arithmetic stops and the tables start
&lt;/h2&gt;

&lt;p&gt;It would be tidy if everything downstream were modular arithmetic. It isn't, and pretending otherwise is how rule-heavy products rot. There are at least two layers that are &lt;em&gt;tables&lt;/em&gt;, not formulas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brightness.&lt;/strong&gt; A star in a given sector has a strength grade — usually rendered as four levels from brightest to dimmest. There is no closed-form function that produces it. It is a lookup keyed on (star, sector), sourced from a classical reference and transcribed once. The engineering risk is transcription error, not arithmetic error, so the only faithful test is a diff against the source table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transformations.&lt;/strong&gt; Four of the stars carry a state that changes with the year (and, at coarser granularity, with a ten-year period). The mapping is again a table keyed on a derived input, and it is &lt;em&gt;injected at read time&lt;/em&gt;, not stored in the chart. Two consecutive years can change the state of a palace without changing a single star position.&lt;/p&gt;

&lt;p&gt;So the correct mental model is a pipeline with clearly separated stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;birth inputs
  -&amp;gt; derived inputs (calendar conversion, hour index)
  -&amp;gt; index arithmetic      (deterministic, assertable)
  -&amp;gt; table lookups         (deterministic, but must be transcribed and diffed)
  -&amp;gt; set expansion         (deterministic, assertable)
  -&amp;gt; interpretation text   (the only non-deterministic stage)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first four stages are computer science. Only the last one is prose. Mixing them — letting a language model re-derive a sector index while it writes the paragraph — is the failure mode that makes otherwise sensible products unreproducible: the same input yields a different number in the second paragraph than in the first, and nobody can tell which one came from the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why separation is the feature, not the housekeeping
&lt;/h2&gt;

&lt;p&gt;Two concrete payoffs, both boring in the way that good infrastructure is boring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproducibility.&lt;/strong&gt; If stages one through four are pure functions of the inputs, then the same birth data always produces the same structural facts, byte for byte, on any machine. Any disagreement is a bug with a bisectable cause: either a derived input differs (calendar conversion, hour convention) or a table entry was transcribed differently. That is a debugging session, not a debate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auditability.&lt;/strong&gt; When the structure is computed and the prose is generated against that structure, the prose can be checked. Does the paragraph mention the same palace set the query returned? Does the strength claim match the table? Where a claim cannot be checked, you can at least see that it came from stage five rather than stage two.&lt;/p&gt;

&lt;p&gt;This is also the honest limit of the whole exercise. The arithmetic decides &lt;em&gt;where&lt;/em&gt; things sit and &lt;em&gt;which&lt;/em&gt; four slots belong together. It does not decide what they mean, and no amount of formula-fiddling will turn a position into advice. Anyone who presents a chart position as a verdict — you will move abroad, you will change careers — has left the deterministic part and is improvising.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it against a real chart
&lt;/h2&gt;

&lt;p&gt;The ring, the six pairs, the four-node set, the two tables — that is the entire mechanical skeleton of a chart. If you want to check the model against live output rather than a diagram, the site's own guides cover how each palace is read, and the calculator will render all twelve sectors from birth data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://askziwei.com/?utm_source=devto" rel="noopener noreferrer"&gt;https://askziwei.com/?utm_source=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generate a chart, take the sector carrying the Life Palace, and compute its four-node set by hand with the modulo expression above. If your hand count and the rendered chart agree, the ring is what this post says it is. If they don't, you have found either a convention difference or a bug — and either way you have learned more than another paragraph of interpretation would have told you.&lt;/p&gt;

&lt;p&gt;If you are building something similar in another rule-heavy domain, the transferable lesson is the layer boundary: compute what can be computed, transcribe what must be transcribed, diff what must be diffed, and keep the prose at the end where it can be checked against the other three.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>math</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Anchor Step: One Mod-12 Formula That Determines Every Position in a Zi Wei Dou Shu Chart</title>
      <dc:creator>taurus yao</dc:creator>
      <pubDate>Thu, 15 Oct 2026 18:31:45 +0000</pubDate>
      <link>https://dev.to/taurusyao/the-anchor-step-one-mod-12-formula-that-determines-every-position-in-a-zi-wei-dou-shu-chart-429o</link>
      <guid>https://dev.to/taurusyao/the-anchor-step-one-mod-12-formula-that-determines-every-position-in-a-zi-wei-dou-shu-chart-429o</guid>
      <description>&lt;p&gt;Every chart built from a fixed rule set has an &lt;em&gt;anchoring step&lt;/em&gt; — the one computation that everything else is indexed to. Get it wrong and no amount of careful reading downstream will reveal why the output looks off. In Zi Wei Dou Shu, that step is locating the &lt;strong&gt;Life Palace&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a short computation, which is exactly why it is worth writing down precisely. It is also the step where two implementations most often disagree &lt;em&gt;without either one making an arithmetic mistake&lt;/em&gt; — the disagreement is usually about the inputs, not the formula. That distinction is worth internalizing if you build anything in a rule-heavy domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the anchor decides
&lt;/h2&gt;

&lt;p&gt;The Life Palace is a position in a twelve-slot cycle (the twelve "palaces", or sectors). It is not a label on a fixed grid. It is the reference point from which the other eleven slots are counted, and two of its properties feed every later layer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Its branch (slot) sets the &lt;strong&gt;order&lt;/strong&gt; of the other eleven.&lt;/li&gt;
&lt;li&gt;Its stem-and-branch pair produces a value called the &lt;strong&gt;Five Element Bureau&lt;/strong&gt;, which is the key the star-placement layer uses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the chain is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;month + hour        -&amp;gt; Life Palace branch
Life Palace + year  -&amp;gt; bureau
bureau + lunar day  -&amp;gt; Zi Wei star position
Zi Wei + Tian Fu    -&amp;gt; all fourteen main stars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One position changes, and every layer below it changes. That is the whole reason the anchor step deserves its own treatment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inputs, stated exactly
&lt;/h2&gt;

&lt;p&gt;Two inputs, and only two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;M&lt;/strong&gt; — the lunar birth month, 1–12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H&lt;/strong&gt; — the birth-hour branch index, counting the first double-hour (Zi) as 0 through 11.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are &lt;em&gt;derived&lt;/em&gt; values, not raw inputs. M comes from a Gregorian-to-lunar conversion. H comes from the birth clock time, optionally corrected to true solar time. If either derivation differs between two implementations, the formula below can be identical and the results will still differ — this is the point people miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;

&lt;p&gt;Using branch indices where Zi = 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Life Palace:  L = (M + 1 - H) mod 12
Body Palace:  S = (M + 1 + H) mod 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are worth noticing before the example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same inputs, opposite direction.&lt;/strong&gt; The classical procedure is: set the Yin palace (branch index 2) as lunar month 1, count &lt;em&gt;forward&lt;/em&gt; to the birth month, then from there count &lt;em&gt;backward&lt;/em&gt; to the birth hour. That backward count is why the formula subtracts H. The Body Palace runs the same count in the &lt;em&gt;opposite&lt;/em&gt; direction — hence the plus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Body Palace has only six reachable slots.&lt;/strong&gt; Because &lt;code&gt;S - L = 2H (mod 12)&lt;/code&gt;, the Body Palace can only land on an even offset from the Life Palace: the Life, Spouse, Wealth, Travel, Career and Fortune palaces. That is a property of the arithmetic, not a rule of interpretation — but it is a fast consistency check when you are validating a chart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example
&lt;/h2&gt;

&lt;p&gt;Take a birth in lunar month 8, at the sixth double-hour (Si, index 5).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Life Palace.&lt;/strong&gt; &lt;code&gt;L = (8 + 1 - 5) mod 12 = 4&lt;/code&gt; -&amp;gt; branch index 4 = Chen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Body Palace.&lt;/strong&gt; &lt;code&gt;S = (8 + 1 + 5) mod 12 = 2&lt;/code&gt; -&amp;gt; branch index 2 = Yin.&lt;/p&gt;

&lt;p&gt;The hand-count version of the same thing, to confirm the formula is not doing something the procedure does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;month forward: Yin(month 1) -&amp;gt; Mao(2) -&amp;gt; Chen(3) -&amp;gt; Si(4) -&amp;gt; Wu(5) -&amp;gt; Wei(6) -&amp;gt; Shen(7) -&amp;gt; You(8)&lt;/li&gt;
&lt;li&gt;hour backward from You: You(hour 0) -&amp;gt; Shen(1) -&amp;gt; Wei(2) -&amp;gt; Wu(3) -&amp;gt; Si(4) -&amp;gt; Chen(5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both paths land the Life Palace on Chen. The Body Palace counts the same six steps forward from You instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You(0) -&amp;gt; Xu(1) -&amp;gt; Hai(2) -&amp;gt; Zi(3) -&amp;gt; Chou(4) -&amp;gt; Yin(5)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and lands on Yin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying out the rest
&lt;/h2&gt;

&lt;p&gt;Once the anchor is fixed, the remaining eleven slots are assigned in reverse branch order starting from the Life Palace. Enumerating that is mechanical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Palace&lt;/th&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Life&lt;/td&gt;
&lt;td&gt;Chen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Siblings&lt;/td&gt;
&lt;td&gt;Mao&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spouse&lt;/td&gt;
&lt;td&gt;Yin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Children&lt;/td&gt;
&lt;td&gt;Chou&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wealth&lt;/td&gt;
&lt;td&gt;Zi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health&lt;/td&gt;
&lt;td&gt;Hai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel&lt;/td&gt;
&lt;td&gt;Xu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Friends&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Career&lt;/td&gt;
&lt;td&gt;Shen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Property&lt;/td&gt;
&lt;td&gt;Wei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fortune&lt;/td&gt;
&lt;td&gt;Wu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parents&lt;/td&gt;
&lt;td&gt;Si&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this example the Body Palace (Yin) coincides with the Spouse slot. That is a normal outcome — it just records where the chart puts its emphasis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "same inputs, different chart" usually is not a bug
&lt;/h2&gt;

&lt;p&gt;This is the part that transfers to any rule engine with historical conventions baked in. The formula is standard. When two tools disagree on a chart, the disagreement is nearly always about &lt;code&gt;M&lt;/code&gt; or &lt;code&gt;H&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count direction.&lt;/strong&gt; Implementations occasionally differ on which direction one of the two movements runs, or which slot the count starts from. Check this first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;True solar time.&lt;/strong&gt; If one method corrects clock time to true solar time, a birth near a double-hour boundary shifts H by one — and the anchor moves with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late Zi-hour births.&lt;/strong&gt; Births between 23:00 and midnight raise a genuine ambiguity: does that double-hour belong to the current day or the next? Both answers are arithmetically valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Month boundaries and leap months.&lt;/strong&gt; If two converters disagree about whether a date falls in lunar month 7 or 8 — a new-moon boundary, or how an intercalary month is numbered — then M differs by one and the anchor moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Display conventions.&lt;/strong&gt; Two tools can agree on the anchor and still look different, because one labels slots by branch and the other by palace name. Compare the branch first; it is unambiguous.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is a mistake. They are places where an old procedure has more than one defensible reading — and a chart is only fully checkable when the reading it used is stated. If you are building in a domain like this, that is the design lesson: &lt;strong&gt;make the conventions explicit and inspectable&lt;/strong&gt;, because the arithmetic being right is not enough for the output to be reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;An anchor step should be small enough to verify by hand and stated precisely enough that two implementations agree. Here the count is four lines of arithmetic, but the failure modes live entirely in the inputs. That is why a comparison of two charts should start at the anchor rather than at the reading: if the anchor differs, the two readings are not describing the same chart, and comparing their conclusions is not a comparison at all.&lt;/p&gt;




&lt;p&gt;I walk through this step and the star rules that follow it in the reference guide here: &lt;a href="https://askziwei.com/blog/en/life-palace-calculation-formula.html?utm_source=devto" rel="noopener noreferrer"&gt;https://askziwei.com/blog/en/life-palace-calculation-formula.html?utm_source=devto&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>math</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>The 5-Step Algorithm Behind a Zi Wei Dou Shu Chart - Worked by Hand, With Every Convention Flagged</title>
      <dc:creator>taurus yao</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:47:47 +0000</pubDate>
      <link>https://dev.to/taurusyao/the-5-step-algorithm-behind-a-zi-wei-dou-shu-chart-worked-by-hand-with-every-convention-flagged-1h84</link>
      <guid>https://dev.to/taurusyao/the-5-step-algorithm-behind-a-zi-wei-dou-shu-chart-worked-by-hand-with-every-convention-flagged-1h84</guid>
      <description>&lt;p&gt;Most chart-based systems are pipelines. A small, well-defined input goes through a fixed sequence of deterministic transforms and comes out the other side as a large, structured artifact. Zi Wei Dou Shu - a Chinese system of charting built on the lunar calendar - is a clean case study, because its arithmetic is fixed and auditable, and almost every genuinely hard question lives at the boundaries rather than in the interpretation.&lt;/p&gt;

&lt;p&gt;This post walks the five mechanical steps on a single worked example, and flags at each step the places where independent implementations legitimately diverge. Those divergence points are the reason two tools can disagree about a chart without either one being wrong, and knowing where they are is what makes a chart checkable in the first place.&lt;/p&gt;

&lt;p&gt;The input is unglamorous: a Gregorian date, a clock time, and a sex. The output is a 12-slot grid (the "palaces"), each slot carrying a palace name, a branch, a stem, a Five Element Bureau, and a set of stars. Everything in between is arithmetic.&lt;/p&gt;

&lt;p&gt;Worked example throughout: born 12 September 1992, 10:20, female.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Convert the birth data, and decide about true solar time
&lt;/h2&gt;

&lt;p&gt;The chart is built on the lunar calendar, not the Gregorian one, so the first step converts the civil date to a lunar date and expresses the birth time as a &lt;em&gt;double-hour&lt;/em&gt; (a two-hour block), not as clock minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worked example: 1992-09-12 10:20 becomes lunar year 1992, month 8, day 16, in the Si double-hour. The year pillar is Ren-Shen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step looks objective and is not entirely so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;True solar time.&lt;/strong&gt; Clock time is not solar time. A 10:20 birth can fall in a different double-hour once longitude correction (and, in some methods, the equation of time) is applied. Some implementations correct for this, others take the clock as given. Near a boundary, this alone moves the chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the correction is anchored.&lt;/strong&gt; Correction needs a birthplace. Historical moves and the choice of a nearby city both introduce small differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lunar month boundaries.&lt;/strong&gt; Whether a lunar month starts at the astronomical new moon or an adopted civil date can differ by a day between data sources, which is enough to shift the month count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leap months.&lt;/strong&gt; For a birth in an intercalary month, systems differ on whether to count it in its own right or fold it into the preceding month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2 - Locate the Life Palace and the Body Palace
&lt;/h2&gt;

&lt;p&gt;The twelve palaces are anchored by the &lt;em&gt;Life Palace&lt;/em&gt;. The classical procedure runs the month count forward from the Yin palace, then the hour count backward from wherever the month count landed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BRANCHES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Zi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chou&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Yin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mao&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Si&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wei&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Xu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;palaces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lunar_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour_index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# hour_index: 0 = Zi, 1 = Chou, ... 5 = Si, ... 11 = Hai
&lt;/span&gt;    &lt;span class="c1"&gt;# movement 1 - Yin holds month 1, count forward to the birth month
&lt;/span&gt;    &lt;span class="n"&gt;month_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lunar_month&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="c1"&gt;# movement 2 - put Zi at month_pos; life counts backward, body forward
&lt;/span&gt;    &lt;span class="n"&gt;life&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;month_pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;hour_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;month_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hour_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;BRANCHES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;life&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;BRANCHES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;palaces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; ('Chen', 'Yin')
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Worked example, movement 1: Yin as month 1, counting forward to month 8, gives the You palace.&lt;/li&gt;
&lt;li&gt;Movement 2, Life: from You, set Zi there and count backward to the Si hour: You, Shen, Wei, Wu, Si, Chen - the Life Palace is Chen.&lt;/li&gt;
&lt;li&gt;Movement 2, Body: same start, opposite direction: You, Xu, Hai, Zi, Chou, Yin - the Body Palace is Yin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two useful invariants fall out of the arithmetic. First, the Body Palace uses the same starting position as the Life count and runs the other way, so it can only ever land on six of the twelve palaces. Second, the hour index that drives both movements is a double-hour index, so every ambiguity from Step 1 inherits straight into this step.&lt;/p&gt;

&lt;p&gt;Divergence points here: the direction convention used for each of the two movements; which double-hour actually feeds the count (inherited from Step 1); and late-Zi births, between 23:00 and midnight, where implementations disagree on whether that hour belongs to the current day or the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 - Fix the Five Element Bureau
&lt;/h2&gt;

&lt;p&gt;The Life Palace is not only a position - it also takes a stem, assigned by the same stem cycle that assigns stems to the months of the year: the year stem anchors the Yin palace and the cycle runs forward from there. Life Palace stem and branch together form a sexagenary pair, and that pair's associated element and number give the Five Element Bureau.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worked example: the year stem is Ren. Anchoring Yin with Ren and counting forward: Yin = Ren, Mao = Gui, Chen = Jia. The Life Palace is Jia-Chen, the pair for the Fire Six Bureau.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Bureau&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Water&lt;/td&gt;
&lt;td&gt;Water Two&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wood&lt;/td&gt;
&lt;td&gt;Wood Three&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metal&lt;/td&gt;
&lt;td&gt;Metal Four&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earth&lt;/td&gt;
&lt;td&gt;Earth Five&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fire&lt;/td&gt;
&lt;td&gt;Fire Six&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That number is not decorative. The next step consumes it.&lt;/p&gt;

&lt;p&gt;Divergence at this step is almost always inherited: the mapping is a standard table, so a different bureau usually means Step 2 produced a different Life Palace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 - Place the Zi Wei star
&lt;/h2&gt;

&lt;p&gt;Zi Wei is positioned by a two-input lookup: the bureau number and the lunar day of birth. Given both, a fixed table returns the palace where Zi Wei sits. This star anchors the largest group of the main stars.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worked example: bureau Fire Six, lunar day 16 -&amp;gt; Zi Wei sits in the Wu palace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Divergence points: published tables have historically differed, especially for higher bureau numbers and the later days of a lunar month; and the wrap-around adjustment some presentations apply at month end varies in how aggressive it is. Because everything in Step 5 hangs off Zi Wei, a one-palace difference here propagates widely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 - Distribute the fourteen main stars
&lt;/h2&gt;

&lt;p&gt;The fourteen main stars travel in two groups. The Zi Wei group is placed relative to Zi Wei, running stepwise in one direction. The Tian Fu group is anchored by Tian Fu, positioned by a mirror relationship to Zi Wei, and runs stepwise in the opposite direction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worked example: Zi Wei in Wu, Tian Fu in Xu. Applying the two sequences gives:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Palace&lt;/th&gt;
&lt;th&gt;Main star(s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Life&lt;/td&gt;
&lt;td&gt;Qi Sha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Siblings&lt;/td&gt;
&lt;td&gt;Tai Yang, Tian Liang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spouse&lt;/td&gt;
&lt;td&gt;Wu Qu, Tian Xiang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Children&lt;/td&gt;
&lt;td&gt;Tian Tong, Ju Men&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wealth&lt;/td&gt;
&lt;td&gt;Tan Lang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health&lt;/td&gt;
&lt;td&gt;Tai Yin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel&lt;/td&gt;
&lt;td&gt;Lian Zhen, Tian Fu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Friends&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Career&lt;/td&gt;
&lt;td&gt;Po Jun&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Property&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fortune&lt;/td&gt;
&lt;td&gt;Zi Wei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parents&lt;/td&gt;
&lt;td&gt;Tian Ji&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two palaces hold no main star at all, which is normal: twelve palaces outnumber fourteen stars, and empty palaces are read through their opposite palace and the surrounding triangle rather than treated as blank.&lt;/p&gt;

&lt;p&gt;Divergence points: which auxiliary and minor stars are included at all (the widest area of disagreement); how the Four Transformations that arise from the decade and the year are treated versus those from birth; and how much of the result a tool chooses to display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two tools can disagree without either being wrong
&lt;/h2&gt;

&lt;p&gt;Collect the divergence points and the picture is clean. Two charts for the same birth data can differ because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether true solar time was applied, and how;&lt;/li&gt;
&lt;li&gt;how the double-hour boundary and the late-Zi hour were treated;&lt;/li&gt;
&lt;li&gt;lunar month boundary and leap month handling;&lt;/li&gt;
&lt;li&gt;the direction convention used for the Life and Body palaces;&lt;/li&gt;
&lt;li&gt;which published table variant placed Zi Wei;&lt;/li&gt;
&lt;li&gt;which auxiliary and minor stars were included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost none of these are disagreements about interpretation. They are disagreements about input and mechanics, and they happen before interpretation begins. That is why a comparison that looks only at the final reading is so often confusing: the two readings may not be describing the same chart at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check a calculator
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce Steps 1 to 3 by hand.&lt;/strong&gt; They are arithmetic you can do on paper. If a tool's Life Palace or bureau disagrees with your own count, stop and find out why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test a boundary case.&lt;/strong&gt; Pick a birth a few minutes either side of a double-hour edge and see whether the chart moves. That tells you immediately whether solar-time correction is in play.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask what is being shown.&lt;/strong&gt; A sparse chart may simply be filtered, not wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer tools that state their conventions.&lt;/strong&gt; A chart with visible assumptions can be reasoned about; one with hidden assumptions cannot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the calculation does not do
&lt;/h2&gt;

&lt;p&gt;All of the above produces a chart. It does not produce a reading. Interpretation is a separate layer: relationships between palaces, the transformations stars carry, and the decade and year cycles that activate different parts of the chart over time. Keeping the two layers distinct is the same discipline as keeping a deterministic core separate from the layer that narrates it, and it is what makes the core testable.&lt;/p&gt;

&lt;p&gt;If you want to run the whole sequence on your own birth data, the full step-by-step walkthrough behind this post lives here: &lt;a href="https://askziwei.com/blog/en/how-to-calculate-zi-wei-dou-shu-chart.html?utm_source=devto" rel="noopener noreferrer"&gt;How a Zi Wei Dou Shu Chart Is Calculated&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>math</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Let the engine do the math: a two-stage architecture for rule-heavy LLM products</title>
      <dc:creator>taurus yao</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:27:16 +0000</pubDate>
      <link>https://dev.to/taurusyao/let-the-engine-do-the-math-a-two-stage-architecture-for-rule-heavy-llm-products-3970</link>
      <guid>https://dev.to/taurusyao/let-the-engine-do-the-math-a-two-stage-architecture-for-rule-heavy-llm-products-3970</guid>
      <description>&lt;p&gt;Most LLM products in rule-heavy domains break the same way: they let the model do the arithmetic.&lt;/p&gt;

&lt;p&gt;I build an app that reads Chinese metaphysics charts (Zi Wei Dou Shu / BaZi). It's a good stress test for this problem, because the domain splits cleanly in two. The &lt;strong&gt;calculation&lt;/strong&gt; side is fully deterministic — given birth date, birth time and gender there is exactly one correct chart, and the rules that produce it are fixed tables that haven't moved in centuries. The &lt;strong&gt;interpretation&lt;/strong&gt; side is prose. Those two jobs want completely different machinery, and the bugs show up the moment you let them blur.&lt;/p&gt;

&lt;p&gt;Here's the separation I landed on, plus the parts that were not obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: plausible instead of correct
&lt;/h2&gt;

&lt;p&gt;An LLM asked to "generate a chart" will produce something that &lt;em&gt;looks&lt;/em&gt; like a chart. The star names are right. The palace structure is right. The formatting is beautiful. And the placements are wrong, because placement depends on calendar facts the model has no way to compute reliably.&lt;/p&gt;

&lt;p&gt;The nasty part is that nobody can tell by looking. A user who doesn't already know their chart can't audit it. So wrong output and right output are indistinguishable at the UI level — which means the whole product is unverifiable, no matter how polished the prose on top is.&lt;/p&gt;

&lt;p&gt;That's the actual defect. Not "the AI is inaccurate" — the output is &lt;em&gt;unauditable&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: compute, don't generate
&lt;/h2&gt;

&lt;p&gt;Stage 1 takes birth data and returns a chart. Every value comes from a fixed rule table or a calendar conversion. No model call, no sampling, no temperature.&lt;/p&gt;

&lt;p&gt;Two things make this stage harder than it sounds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calendar conversion is the real trap.&lt;/strong&gt; Converting a Gregorian birth date into the calendar system the rules operate on is not a formatting problem. You're dealing with historical timezone offsets, daylight-saving periods that existed in some regions and not others, and the difference between clock time and true solar time — which is a function of longitude and the equation of time. Get any of those wrong and every downstream placement shifts by one palace. I stopped writing this myself and pinned a well-tested calendar library instead, because the edge cases are historical data, not logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule tables have to be data, not prompt text.&lt;/strong&gt; Star placement is a set of lookup rules keyed on things like the value and parity of specific cycle indices. If those tables live in a prompt, they're re-interpreted on every call and drift. If they live in code as tables, they're diffable, testable, and reviewable — and when a disputed rule shows up, you can point at one line.&lt;/p&gt;

&lt;p&gt;The output of Stage 1 is a plain data structure. That matters for what comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: the model explains what already exists
&lt;/h2&gt;

&lt;p&gt;Stage 2 receives the computed chart and produces human language. It explains what a star sitting in a particular palace means in this system. It does &lt;strong&gt;not&lt;/strong&gt; decide what's in the palace — that's already settled.&lt;/p&gt;

&lt;p&gt;This split buys a specific property: &lt;strong&gt;the reading can be traced back to a fixed rule.&lt;/strong&gt; When someone asks "why does it say that?", the chain is: this sentence interpreted &lt;em&gt;this&lt;/em&gt; fact, and &lt;em&gt;this&lt;/em&gt; fact came from &lt;em&gt;this&lt;/em&gt; table at &lt;em&gt;this&lt;/em&gt; line. Not "the model felt that way."&lt;/p&gt;

&lt;p&gt;I'm deliberately not claiming the interpretation is objective — it's a traditional reading system, and reasonable practitioners read the same chart differently. What I &lt;em&gt;can&lt;/em&gt; claim is that the mechanical layer is reproducible and the interpretation layer is constrained to it. Those are separate promises and it's worth keeping them separate in how you talk about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing is unusually easy here, and that's the tell
&lt;/h2&gt;

&lt;p&gt;Because Stage 1 is deterministic, tests are exact-match fixtures: input birth data → expected chart. No fuzzy scoring, no LLM-as-judge, no "does this look reasonable."&lt;/p&gt;

&lt;p&gt;That property is a good smell test for architecture. If your core layer needs a model to grade it, you don't have a deterministic core — you have a second model, and now you need to validate &lt;em&gt;that&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The interpretation layer is different and I test it differently: regression fixtures on the facts handed to the model, plus checks that it never contradicts the computed chart. Checking "did the model stay inside the box it was given" is a much better-behaved test than trying to grade prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring infrastructure on purpose
&lt;/h2&gt;

&lt;p&gt;The whole thing runs as a small Python service behind a tunnel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one application process, bound to &lt;code&gt;127.0.0.1&lt;/code&gt; only — the tunnel is the only way in, so there's no directly exposed port to misconfigure&lt;/li&gt;
&lt;li&gt;SQLite for state, single file, included in the daily backup rather than run as a server&lt;/li&gt;
&lt;li&gt;a process supervisor that restarts the app if it dies, plus a separate watchdog that checks the app answers before declaring anything healthy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two operational lessons that cost me real time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The service responded 200" is not the same as "the supervisor is running the service."&lt;/strong&gt; You can end up with a stray process serving traffic while the managed one is dead, and then the next restart takes down a service you thought was supervised. Compare the supervisor's PID against the PID actually holding the port. If they differ, you have a ghost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A watchdog that auto-restarts on a timer will happily restart you mid-deploy.&lt;/strong&gt; Pause it during deployments and restore it after, with a trap so it comes back even if the deploy fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd pass on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your domain has one correct answer, compute it in code and never let the model near it.&lt;/li&gt;
&lt;li&gt;Make the mechanical layer auditable before you invest in prose quality. Prose on top of an unauditable core is a confidence trick, even when it's accidental.&lt;/li&gt;
&lt;li&gt;Exact-match fixtures are a gift. If your core can't have them, that's worth investigating.&lt;/li&gt;
&lt;li&gt;Keep your claims as narrow as what you can actually verify. "Reproducible computation" and "meaningful interpretation" are different promises; conflating them is how products end up overclaiming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-stage split is the only structural decision in this project I'd defend without caveats. Everything else is details.&lt;/p&gt;




&lt;p&gt;I write about this build in more detail — architecture, the bilingual pipeline, and the calendar edge cases — at &lt;a href="https://askziwei.com" rel="noopener noreferrer"&gt;askziwei.com&lt;/a&gt;. If you're working on a rule-heavy LLM product, I'd genuinely like to hear how you drew the line between computed and generated.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
