Skip to content

Instantly share code, notes, and snippets.

@alex-quiterio
Created July 4, 2026 15:18
Show Gist options
  • Select an option

  • Save alex-quiterio/46ec80beae5132376b5683a0eefa1dca to your computer and use it in GitHub Desktop.

Select an option

Save alex-quiterio/46ec80beae5132376b5683a0eefa1dca to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# frozen_string_literal: true
# =============================================================================
# human_design.rb — compute a Human Design chart (Type, Authority, Profile,
# defined Centers, active gates/channels) from birth data.
#
# Human Design is derived from TWO charts:
# 1. Personality (conscious): the exact moment of birth.
# 2. Design (unconscious): the moment the Sun was exactly 88 degrees of arc
# BEFORE its birth position (~88 days before birth).
# The 13 planetary activations from each are mapped onto the 64 I Ching gates,
# then onto channels and the 9 centers.
#
# ---------------------------------------------------------------------------
# SETUP (run once in your own terminal — the sandbox that generated this
# file cannot install gems or run Ruby):
#
# gem install swe4r
#
# swe4r bundles the Swiss Ephemeris, so no extra data files are needed for
# dates within a few centuries of today.
#
# RUN:
# ruby human_design.rb
#
# The script will prompt you interactively for birth date, time, and UTC offset.
#
# Pass --with-num to also compute a Pythagorean numerology reading (Life Path,
# Birthday, Expression, Soul Urge, Personality numbers), which additionally
# prompts for your full name:
# ruby human_design.rb --with-num
# =============================================================================
require "date"
begin
require "swe4r"
rescue LoadError
abort <<~MSG
The 'swe4r' gem is not installed.
Install it first with:
gem install swe4r
then re-run: ruby human_design.rb
MSG
end
module HumanDesign
# Interactive stdin prompts shared by anything that needs user input.
# Uses STDIN explicitly rather than bare `gets`, since bare `gets` reads
# from files named in ARGV (e.g. "--with-num") instead of the terminal.
module Prompt
module_function
def ask(prompt)
print "#{prompt}: "
$stdout.flush
STDIN.gets&.strip
end
def integer(prompt, range)
loop do
value = ask(prompt)
return value.to_i if value =~ /\A-?\d+\z/ && range.include?(value.to_i)
puts " Please enter a whole number in #{range}."
end
end
def float(prompt)
loop do
value = ask(prompt)
return value.to_f if value =~ /\A-?\d+(\.\d+)?\z/
puts " Please enter a number."
end
end
def text(prompt)
loop do
value = ask(prompt)
return value if value && !value.empty?
puts " Please enter a value."
end
end
end
# Wraps the Swiss Ephemeris calls needed to get a body's ecliptic longitude.
class Ephemeris
FLAGS = Swe4r::SEFLG_SWIEPH | Swe4r::SEFLG_SPEED
def self.julian_day(year, month, day, hour, min, utc_offset)
ut_hour = hour + min / 60.0 - utc_offset
Swe4r.swe_julday(year, month, day, ut_hour)
end
# Ecliptic longitude (0..360) of one body at a given Julian Day.
def self.longitude(jd, body)
case body
when :earth
(longitude(jd, Swe4r::SE_SUN) + 180.0) % 360.0
when :south_node
(longitude(jd, Swe4r::SE_TRUE_NODE) + 180.0) % 360.0
else
Swe4r.swe_calc_ut(jd, body, FLAGS)[0] % 360.0
end
end
end
# Local civil birth date/time, entered interactively.
class BirthData
attr_reader :year, :month, :day, :hour, :min, :utc_offset
def self.prompt
puts "Enter birth data (local clock date/time):"
data = new(
year: Prompt.integer("Birth year (e.g. 1990)", 1800..2100),
month: Prompt.integer("Birth month (1-12)", 1..12),
day: Prompt.integer("Birth day (1-31)", 1..31),
hour: Prompt.integer("Birth hour, 24h local clock (0-23)", 0..23),
min: Prompt.integer("Birth minute (0-59)", 0..59),
utc_offset: Prompt.float("UTC offset in hours (e.g. 1 for UTC+1, -5 for UTC-5)")
)
puts
data
end
def initialize(year:, month:, day:, hour:, min:, utc_offset:)
@year = year
@month = month
@day = day
@hour = hour
@min = min
@utc_offset = utc_offset
end
def julian_day
Ephemeris.julian_day(year, month, day, hour, min, utc_offset)
end
def to_s
"#{day}/#{month}/#{year} " \
"#{format('%02d:%02d', hour, min)} " \
"(UTC#{format('%+d', utc_offset)})"
end
end
# One of the 64 I Ching gates plus a line number (1..6), derived from an
# ecliptic longitude.
class Gate
# The 64 gates in zodiacal order around the 360° ecliptic. HD places
# Gate 41 at the start (2° Aquarius region); each gate spans 5.625°.
WHEEL = [
41, 19, 13, 49, 30, 55, 37, 63, 22, 36, 25, 17, 21, 51, 42, 3,
27, 24, 2, 23, 8, 20, 16, 35, 45, 12, 15, 52, 39, 53, 62, 56,
31, 33, 7, 4, 29, 59, 40, 64, 47, 6, 46, 18, 48, 57, 32, 50,
28, 44, 1, 43, 14, 34, 9, 5, 26, 11, 10, 58, 38, 54, 61, 60
].freeze
# The wheel starts at this absolute ecliptic longitude (start of Gate 41).
# 302° = 2° Aquarius. Standard offset used by mainstream HD software.
START_DEG = 302.0
SIZE = 360.0 / 64.0 # 5.625°
LINE_SIZE = SIZE / 6.0 # each gate has 6 lines
attr_reader :number, :line
def self.at(longitude)
offset = (longitude - START_DEG) % 360.0
idx = (offset / SIZE).floor
within = offset - idx * SIZE
new(WHEEL[idx], (within / LINE_SIZE).floor + 1)
end
def initialize(number, line)
@number = number
@line = line
end
def center
Center.containing(number)
end
def to_s
"#{number}.#{line}"
end
end
# One of the 9 centers in the BodyGraph, and which gates belong to it.
class Center
GATES_BY_NAME = {
"Head" => [64, 61, 63],
"Ajna" => [47, 24, 4, 17, 43, 11],
"Throat" => [62, 23, 56, 35, 12, 45, 33, 8, 31, 20, 16],
"G" => [1, 13, 25, 46, 2, 15, 10, 7],
"Heart" => [21, 40, 26, 51], # Ego / Will
"Spleen" => [48, 57, 44, 50, 32, 28, 18],
"SolarPlexus" => [6, 37, 22, 36, 30, 55, 49], # Emotional
"Sacral" => [34, 5, 14, 29, 59, 9, 3, 42, 27],
"Root" => [58, 38, 54, 53, 60, 52, 19, 39, 41]
}.freeze
ALL = GATES_BY_NAME.keys.freeze
MOTORS = %w[Sacral Heart SolarPlexus Root].freeze
def self.containing(gate_number)
GATES_BY_NAME.find { |_name, gates| gates.include?(gate_number) }&.first
end
end
# One of the 36 channels connecting two gates (and thus two centers).
class Channel
CENTERS_BY_GATES = {
[1, 8] => %w[G Throat],
[2, 14] => %w[G Sacral],
[3, 60] => %w[Sacral Root],
[4, 63] => %w[Ajna Head],
[5, 15] => %w[Sacral G],
[6, 59] => %w[SolarPlexus Sacral],
[7, 31] => %w[G Throat],
[9, 52] => %w[Sacral Root],
[10, 20] => %w[G Throat],
[10, 34] => %w[G Sacral],
[10, 57] => %w[G Spleen],
[11, 56] => %w[Ajna Throat],
[12, 22] => %w[Throat SolarPlexus],
[13, 33] => %w[G Throat],
[16, 48] => %w[Throat Spleen],
[17, 62] => %w[Ajna Throat],
[18, 58] => %w[Spleen Root],
[19, 49] => %w[Root SolarPlexus],
[20, 34] => %w[Throat Sacral],
[20, 57] => %w[Throat Spleen],
[21, 45] => %w[Heart Throat],
[23, 43] => %w[Throat Ajna],
[24, 61] => %w[Ajna Head],
[25, 51] => %w[G Heart],
[26, 44] => %w[Heart Spleen],
[27, 50] => %w[Sacral Spleen],
[28, 38] => %w[Spleen Root],
[29, 46] => %w[Sacral G],
[30, 41] => %w[SolarPlexus Root],
[32, 54] => %w[Spleen Root],
[34, 57] => %w[Sacral Spleen],
[35, 36] => %w[Throat SolarPlexus],
[37, 40] => %w[SolarPlexus Heart],
[39, 55] => %w[Root SolarPlexus],
[42, 53] => %w[Sacral Root],
[47, 64] => %w[Ajna Head]
}.freeze
attr_reader :gates, :centers
def self.all
@all ||= CENTERS_BY_GATES.map { |gates, centers| new(gates, centers) }
end
def initialize(gates, centers)
@gates = gates
@centers = centers
end
def includes_gate?(gate_number)
gates.include?(gate_number)
end
def active?(active_gate_numbers)
gates.all? { |g| active_gate_numbers.include?(g) }
end
def to_s
"#{gates.min}-#{gates.max} (#{centers.join(' <-> ')})"
end
end
# The 13 bodies used in HD (Sun..Pluto + the lunar Nodes), in chart order.
class Planet
BODY_BY_NAME = {
"Sun" => Swe4r::SE_SUN,
"Earth" => :earth, # Sun + 180°
"Moon" => Swe4r::SE_MOON,
"NorthNode" => Swe4r::SE_TRUE_NODE,
"SouthNode" => :south_node, # NorthNode + 180°
"Mercury" => Swe4r::SE_MERCURY,
"Venus" => Swe4r::SE_VENUS,
"Mars" => Swe4r::SE_MARS,
"Jupiter" => Swe4r::SE_JUPITER,
"Saturn" => Swe4r::SE_SATURN,
"Uranus" => Swe4r::SE_URANUS,
"Neptune" => Swe4r::SE_NEPTUNE,
"Pluto" => Swe4r::SE_PLUTO
}.freeze
ALL = BODY_BY_NAME.keys.freeze
attr_reader :name
def initialize(name)
@name = name
end
def body
BODY_BY_NAME[name]
end
def gate_at(jd)
Gate.at(Ephemeris.longitude(jd, body))
end
end
# All 13 planetary gate/line activations for one moment in time (either the
# Personality/conscious chart or the Design/unconscious chart).
class ActivationSet
def initialize(jd)
@jd = jd
@gates_by_planet = Planet::ALL.each_with_object({}) do |name, hash|
hash[name] = Planet.new(name).gate_at(jd)
end
end
def gate_for(planet_name)
@gates_by_planet.fetch(planet_name)
end
def sun_gate
gate_for("Sun")
end
def each_gate(&block)
@gates_by_planet.each(&block)
end
def gate_numbers
@gates_by_planet.values.map(&:number).uniq
end
end
# Finds the Design Julian Day: the moment the Sun was exactly 88° of arc
# before its birth longitude. Solved by bisection (~88 days earlier).
class DesignMoment
ARC_DEGREES = 88.0
SEARCH_WINDOW = (95.0..80.0)
ITERATIONS = 40
def self.julian_day(birth_jd)
new(birth_jd).julian_day
end
def initialize(birth_jd)
@birth_jd = birth_jd
@target = (Ephemeris.longitude(birth_jd, Swe4r::SE_SUN) - ARC_DEGREES) % 360.0
end
def julian_day
lo = @birth_jd - SEARCH_WINDOW.begin
hi = @birth_jd - SEARCH_WINDOW.end
ITERATIONS.times do
mid = (lo + hi) / 2.0
diff = signed_difference(Ephemeris.longitude(mid, Swe4r::SE_SUN))
diff.positive? ? hi = mid : lo = mid
end
(lo + hi) / 2.0
end
private
def signed_difference(longitude)
((longitude - @target + 180.0) % 360.0) - 180.0
end
end
# Given the set of active gate numbers (from both charts), works out which
# channels are complete, which centers are defined, and how they connect.
class BodyGraph
def initialize(active_gate_numbers)
@active_gate_numbers = active_gate_numbers.uniq
@active_channels = Channel.all.select { |c| c.active?(@active_gate_numbers) }
@defined_centers = @active_channels.flat_map(&:centers).uniq
@adjacency = build_adjacency
end
attr_reader :active_gate_numbers, :active_channels, :defined_centers
def undefined_centers
Center::ALL - defined_centers
end
def hanging_gates
@hanging_gates ||= active_gate_numbers.reject do |g|
active_channels.any? { |c| c.includes_gate?(g) }
end
end
def hanging_gates_by_center
hanging_gates.group_by { |g| Center.containing(g) }
end
def connected?(from_center, to_center)
return false unless defined_centers.include?(from_center)
reachable_from(from_center).include?(to_center)
end
def components
seen = []
defined_centers.each_with_object([]) do |center, comps|
next if seen.include?(center)
comp = reachable_from(center)
seen.concat(comp)
comps << comp
end
end
def definition_type
case components.size
when 0 then "No Definition"
when 1 then "Single Definition"
when 2 then "Split Definition"
when 3 then "Triple Split Definition"
when 4 then "Quadruple Split Definition"
else "#{components.size}-Way Split Definition"
end
end
def motor_connected_to_throat?
return false unless defined_centers.include?("Throat")
Center::MOTORS.any? { |m| defined_centers.include?(m) && connected?(m, "Throat") }
end
private
def build_adjacency
adj = Hash.new { |h, k| h[k] = [] }
active_channels.each do |c|
a, b = c.centers
adj[a] << b
adj[b] << a
end
adj
end
def reachable_from(start)
seen = []
stack = [start]
until stack.empty?
n = stack.pop
next if seen.include?(n)
seen << n
@adjacency[n].each { |m| stack << m unless seen.include?(m) }
end
seen
end
end
# The full reading: Type, Strategy, Authority, Profile, Definition — derived
# from a BirthData's Personality and Design activation sets.
class Chart
STRATEGIES = {
"Generator" => "To respond",
"Manifesting Generator" => "To respond, then inform",
"Manifestor" => "To inform before you act",
"Projector" => "Wait for the invitation",
"Reflector" => "Wait a lunar cycle before deciding"
}.freeze
NOT_SELF_THEMES = {
"Generator" => "Frustration",
"Manifesting Generator" => "Frustration and Anger",
"Manifestor" => "Anger",
"Projector" => "Bitterness",
"Reflector" => "Disappointment"
}.freeze
attr_reader :birth, :personality, :design, :body_graph
def initialize(birth_data)
@birth = birth_data
birth_jd = birth_data.julian_day
@personality = ActivationSet.new(birth_jd)
@design = ActivationSet.new(DesignMoment.julian_day(birth_jd))
@body_graph = BodyGraph.new(personality.gate_numbers + design.gate_numbers)
end
def type
return "Reflector" if body_graph.defined_centers.empty?
if body_graph.defined_centers.include?("Sacral")
body_graph.motor_connected_to_throat? ? "Manifesting Generator" : "Generator"
elsif body_graph.motor_connected_to_throat?
"Manifestor"
else
"Projector"
end
end
def strategy
STRATEGIES.fetch(type)
end
def not_self_theme
NOT_SELF_THEMES.fetch(type)
end
# Highest-priority defined center present.
def authority
return "Emotional (Solar Plexus)" if center_defined?("SolarPlexus")
return "Sacral" if center_defined?("Sacral")
return "Splenic" if center_defined?("Spleen")
return "Ego / Heart" if center_defined?("Heart")
return "Self-Projected (G)" if center_defined?("G")
return "Lunar" if type == "Reflector"
"Mental / Environmental (no inner authority)"
end
def profile
"#{personality.sun_gate.line}/#{design.sun_gate.line}"
end
private
def center_defined?(name)
body_graph.defined_centers.include?(name)
end
end
# Pythagorean numerology reading, derived from a full name and BirthData.
# Only ASCII letters A-Z contribute to the letter-based numbers.
class Numerology
LETTER_VALUES = {
"A" => 1, "B" => 2, "C" => 3, "D" => 4, "E" => 5, "F" => 6, "G" => 7, "H" => 8, "I" => 9,
"J" => 1, "K" => 2, "L" => 3, "M" => 4, "N" => 5, "O" => 6, "P" => 7, "Q" => 8, "R" => 9,
"S" => 1, "T" => 2, "U" => 3, "V" => 4, "W" => 5, "X" => 6, "Y" => 7, "Z" => 8
}.freeze
VOWELS = %w[A E I O U].freeze
MASTER_NUMBERS = [11, 22, 33].freeze
attr_reader :full_name, :birth
def initialize(full_name, birth)
@full_name = full_name
@birth = birth
end
# Reduce day, month, and year separately, then sum and reduce again.
def life_path_number
reduce(reduce(birth.day) + reduce(birth.month) + reduce(birth.year))
end
def birthday_number
reduce(birth.day)
end
def expression_number
reduce(letter_values(letters).sum)
end
def soul_urge_number
reduce(letter_values(letters.select { |l| VOWELS.include?(l) }).sum)
end
def personality_number
reduce(letter_values(letters.reject { |l| VOWELS.include?(l) }).sum)
end
private
def letters
full_name.upcase.scan(/[A-Z]/)
end
def letter_values(chars)
chars.map { |c| LETTER_VALUES.fetch(c) }
end
def reduce(number)
loop do
return number if number <= 9 || MASTER_NUMBERS.include?(number)
number = number.digits.sum
end
end
end
# Formats a Chart as plain-text output.
class ChartReport
def initialize(chart, numerology: nil)
@chart = chart
@numerology = numerology
end
def render
render_header
puts
render_summary
puts
render_centers
puts
render_channels
puts
render_hanging_gates
puts
render_activations
puts
render_footer
if numerology
puts
render_numerology
end
end
private
attr_reader :chart, :numerology
def render_header
puts "=" * 60
puts "HUMAN DESIGN CHART"
puts "Birth: #{chart.birth}"
puts "=" * 60
end
def render_summary
bg = chart.body_graph
puts "TYPE : #{chart.type}"
puts "STRATEGY : #{chart.strategy}"
puts "NOT-SELF THEME: #{chart.not_self_theme}"
puts "AUTHORITY : #{chart.authority}"
puts "PROFILE : #{chart.profile}"
puts "DEFINITION : #{bg.definition_type}"
return unless bg.components.size > 1
puts " #{bg.components.map { |c| c.sort.join('+') }.join(' | ')}"
end
def render_centers
bg = chart.body_graph
puts "DEFINED CENTERS (#{bg.defined_centers.size}/9):"
puts " #{bg.defined_centers.sort.join(', ')}"
puts
puts "OPEN / UNDEFINED CENTERS:"
puts " #{bg.undefined_centers.sort.join(', ')}"
end
def render_channels
channels = chart.body_graph.active_channels
puts "ACTIVE CHANNELS (#{channels.size}):"
channels.sort_by { |c| c.gates.min }.each { |c| puts " #{c}" }
end
def render_hanging_gates
bg = chart.body_graph
puts "HANGING GATES (#{bg.hanging_gates.size}):"
if bg.hanging_gates.empty?
puts " none"
else
bg.hanging_gates_by_center.sort.each do |center, gates|
puts " #{center}: #{gates.sort.join(', ')}"
end
end
end
def render_activations
puts "ACTIVATIONS (Personality = conscious, Design = unconscious):"
puts format(" %-10s %-12s %-12s", "", "Personality", "Design")
Planet::ALL.each do |name|
p_gate = chart.personality.gate_for(name)
d_gate = chart.design.gate_for(name)
puts format(" %-10s %-12s %-12s", name, p_gate.to_s, d_gate.to_s)
end
end
def render_footer
puts "Note: verify against myBodyGraph or Genetic Matrix. The wheel offset"
puts "and node method (True Node) match mainstream HD software; tiny"
puts "differences near a gate/line boundary can shift a line by one."
end
def render_numerology
puts "-" * 60
puts "NUMEROLOGY (#{numerology.full_name})"
puts "-" * 60
puts "LIFE PATH : #{numerology.life_path_number}"
puts "BIRTHDAY : #{numerology.birthday_number}"
puts "EXPRESSION : #{numerology.expression_number}"
puts "SOUL URGE : #{numerology.soul_urge_number}"
puts "PERSONALITY : #{numerology.personality_number}"
end
end
end
if __FILE__ == $PROGRAM_NAME
with_numerology = ARGV.include?("--with-num")
birth = HumanDesign::BirthData.prompt
chart = HumanDesign::Chart.new(birth)
numerology = nil
if with_numerology
full_name = HumanDesign::Prompt.text("Full name (for numerology)")
numerology = HumanDesign::Numerology.new(full_name, birth)
puts
end
HumanDesign::ChartReport.new(chart, numerology: numerology).render
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment