# SPDX-License-Identifier: MIT # architecture — name a building's style, keep a record of height, and decide # that a tower tips, a hull is unstable, a room rings too long or too short, and # a channel falls too little. # # THIS MODULE IS NOT ENGINEERING, NAVAL-ARCHITECTURE, ACOUSTIC-DESIGN OR # HERITAGE ADVICE. It states published definitions and simple models as rules # over the facts you record. A tower here is a straight column of even weight, # a hull is a box, and a room is Sabine's equation: whether a real structure is # safe, a real ship stable or a real hall good for music is decided by those who # survey, test and certify it. A style is a family resemblance that a rule # makes strict, and a record of height is a verdict over the records you wrote. # # Two halves that share nothing but the subject. # # STRUCTURE. You record facts; standing rules conclude, each with its proof. # # Buildings (`architecture.Building`), each with the features recorded for it. # `building()` writes all nine features, True for those you name and False for # the rest, so a building you described lacks what you did not name and a # building you never described is unknown. `gothic` is concluded where a # building has pointed arches, rib vaults and flying buttresses, the three of # the four features Gothic's definition names that are parts of the structure # (the fourth is its windows); `romanesque` where it has round arches, thick # walls and a barrel vault or a groin vault; `pendentive_dome` where it has a # dome and pendentives. A pendentive is a device and not a period, so the last # is a form, not a style. `why_not` names the feature a building lacks: # # import architecture # architecture.building("cologne", ["pointed_arch", "rib_vault", "flying_buttress"]) # architecture.building("abbey", ["round_arch", "thick_walls", "pointed_arch", "rib_vault"]) # print(architecture.styles_of("cologne"), architecture.styles_of("abbey")) # n = why_not(architecture.Building.abbey.gothic) # # Structures (`architecture.Structure`), each with a height in metres, the year # it was completed, whether it is a building, and the year it fell if it did. # `tallest_structure` is concluded for a structure that was the tallest when it # was completed: nothing recorded was completed no later, still stood that year # and was taller. `tallest_building` asks the same among buildings only, so a # monument or a tower that is not a building ends a structure's record and not # a building's. A year before the common era is written negative, as -2570 for # 2570 BC; the rules only compare years. A structure with no completion year is # marked `undated` and is never ranked: with no year, nothing could be completed # before it, so it would otherwise be concluded the tallest, and nothing could # block another with it. # `record_until()` gives the year a record passed: # # architecture.structure("lincoln", 160.0, 1311.0, True) # architecture.fell("lincoln", 1548.0) # architecture.structure("cologne", 157.0, 1880.0, True) # architecture.structure("washington", 169.0, 1884.0, False) # architecture.structure("ulm", 161.53, 1890.0, True) # print(architecture.tallest_structures(), architecture.tallest_buildings()) # print(architecture.record_until("cologne", False), architecture.record_until("cologne", True)) # # Towers (`architecture.Tower`), each with its height, its top's distance from # the vertical and its base's outer diameter. On the model of a straight tower # of even weight, its centre of mass is half the top's distance from the # vertical; `tips` is concluded where that passes the edge of the base. The # model sees the structure and not the ground under it. # # Hulls (`architecture.Hull`), each a box with a beam, a draught and the height # of its centre of gravity above the keel. The computed fields give the centre # of buoyancy (half the draught), BM (beam squared over twelve times the # draught) and the metacentric height GM; `unstable` is concluded where GM is # not positive. GM measures initial stability, in the first degrees of heel. # # Rooms (`architecture.Room`), each a volume in cubic metres and a target # range of reverberation times in seconds, and surfaces (`architecture.Surface`) # with the room they line, an area in square metres and an absorption # coefficient. A room's absorption is the sum over its surfaces, its time is # Sabine's, and `too_live` and `too_dead` are concluded outside the range. A # room with no surface recorded is `unmeasured`, and a room a surface is bound # to twice is `disputed` (below). # # Channels (`architecture.Channel`), each a length and a drop in the same unit # and the name of the standard it is held to, and standards # (`architecture.Fall`), each a minimum gradient. `too_flat` is concluded where # the channel's gradient is below its standard's minimum; a channel whose # standard nobody wrote is marked `no_standard`. `vitruvius()` writes the two # readings of Vitruvius's minimum the sources give, half a foot in a hundred and # one in 4,800, as two values of one fact: # # architecture.vitruvius() # architecture.channel("nimes", 50000.0, 17.0, "vitruvius") # print(architecture.flat_channels(), architecture.unsettled()) # # Each conclusion answers `why`, and `why_not` names the premise that failed. A # corrected fact withdraws what rested on it. Where two sources disagree about a # fact, the rules reason about every value in force. A verdict concluded from # what a rule finds holds under any of them: a channel marked too flat is too # flat under at least one value of its standard, and a channel not marked is # within all of them. A verdict concluded from what a rule does not find holds # only if nothing is found under any of them: a structure is the tallest only # if no value of any height puts a taller one beside it, so a structure whose # own height holds two values is never ranked. `record_until()` refuses while a # fact it reads holds two values, and `unsettled()` names every fact you write # that holds two values. # # A sum is the exception, since it adds every value it reads: a surface whose # area two surveys give as 2,000 and 2,500 square metres would be counted under # both, an absorption neither survey gives. So while a surface is bound to a # room twice, as two values of its area or coefficient bind it, the room is # `disputed`: no absorption, time or verdict is concluded for it, # `disputed_rooms()` names it, and # `why_not` on its absorption names the dispute. Forgetting the claim that is # not to stand settles it. A surface whose room two sources disagree on lines # each room a value names, so each of those rooms' absorption is one source's, # and a room left unmarked is within the source that puts the surface in it. # # update(architecture.Surface("s1").area, 2500.0, "survey2") # print(architecture.disputed_rooms()) # forget(architecture.Surface.s1.area, 2500.0, "survey2") # # A # `why_not` over a group-wise absence names a cell of the form # `__none1` "to be absent, and it is present": it means that something # the rule asks to be missing was found. # # CALCULATION. Value scripts that return a value and write nothing: a lean in # degrees and the offset of a top, Sabine's reverberation time, a box hull's # metacentric height, a gradient and the same as one in N, and a pendulum's # period and length at standard gravity. Each step is rounded once: `atan` and # `tan` are correctly rounded, and `degrees` and `radians` give a x 180 / pi # and a x pi / 180 rounded once. So a lean or an offset can differ from # CPython's in the last digit, where CPython rounds twice or its library misses # the correctly rounded value; every other script gives the same digits as # CPython. # # Sources, read 2026-11-09: # - Gothic "is characterised by pointed arches, rib vaults, flying buttresses # and large, traceried stained glass windows" # (https://en.wikipedia.org/wiki/Gothic_architecture). # - Romanesque "is known by its massive quality, thick walls, round arches, # sturdy pillars, barrel vaults, large towers and decorative arcading", and # saw "the development of vaults from barrel and groin vaults to ribbed # vaults" (https://en.wikipedia.org/wiki/Romanesque_architecture). # - A pendentive is "a constructional device permitting the placing of a # circular dome over a square room" # (https://en.wikipedia.org/wiki/Pendentive). # - A record of height is kept for structures and for buildings separately: # the list of the tallest structures built before the twentieth century # gives Cologne Cathedral as the "tallest structure on Earth from 1880 to # 1884, and tallest building on Earth from 1880 to 1890" # (https://en.wikipedia.org/wiki/List_of_tallest_structures_built_before_the_20th_century). # - The metacentric height GM "is calculated as the distance between the # centre of gravity of a ship and its metacentre"; "KM = KB + BM" and # "BM = I/V"; "An excessively low or negative GM increases the risk of a ship # capsizing in rough weather" (https://en.wikipedia.org/wiki/Metacentric_height). # For a box, KB as half the draught and BM as B²/12T follow from those, and # are this module's derivation, not a published formula. # - Sabine's equation, "T = (V/A) · 0.161 s", V in cubic metres and A in # square metres of absorption (https://en.wikipedia.org/wiki/Wallace_Clement_Sabine). # The reverberation article gives the constant as about 0.1611, says the # equation "was developed in the late 1890s in an empirical fashion", and # gives Eyring's beside it (https://en.wikipedia.org/wiki/Reverberation). # The module uses 0.161. # - Vitruvius, in translation: "the bed of the channel must have a fall of not # less than half a foot to a length of one hundred" # (https://penelope.uchicago.edu/Thayer/E/Roman/Texts/Vitruvius/8*.html). # The Roman aqueduct article: "Vitruvius recommends a low gradient of not # less than 1 in 4800 for the channel" # (https://en.wikipedia.org/wiki/Roman_aqueduct). The two disagree, and the # module holds both. # - A pendulum's period is "T ≈ 2π √(L/g)" for a swing much smaller than one # radian (https://en.wikipedia.org/wiki/Pendulum); standard gravity is "a # constant defined by ISO standard 80000 as 9.80665 m/s²" # (https://en.wikipedia.org/wiki/Standard_gravity). import math # ── Buildings and their styles ─────────────────────────────────────────────── class Building: # A building, by a name you choose, with a True or False for each of nine # features, written by `building()`. The rules conclude `gothic`, # `romanesque` and `pendentive_dome`, each True. pointed_arch: bool rib_vault: bool flying_buttress: bool round_arch: bool thick_walls: bool barrel_vault: bool groin_vault: bool dome: bool pendentives: bool gothic: bool romanesque: bool pendentive_dome: bool @rule def gothic_building(): for b in Building: if b.pointed_arch == True and b.rib_vault == True and b.flying_buttress == True: b.gothic = True @rule def romanesque_barrel_vault(): for b in Building: if b.round_arch == True and b.thick_walls == True and b.barrel_vault == True: b.romanesque = True @rule def romanesque_groin_vault(): for b in Building: if b.round_arch == True and b.thick_walls == True and b.groin_vault == True: b.romanesque = True @rule def dome_on_pendentives(): for b in Building: if b.dome == True and b.pendentives == True: b.pendentive_dome = True # ── Structures and the record of height ────────────────────────────────────── class Structure: # A structure, by a name you choose: its height in metres, the year it was # completed, negative before the common era, whether it is a building, and # `until`, the year it fell, which `structure()` writes as 10000 for one # that stands. The rules conclude `tallest_structure`, `tallest_building` # and `undated`, each True. height: real completed: real until: real building: bool tallest_structure: bool tallest_building: bool undated: bool @rule def tallest_structure_when_completed(): for s in Structure: if s.height > 0.0 and s.completed > -1000000.0 and none(o in Structure: o.completed <= s.completed and o.until > s.completed and o.height > s.height): s.tallest_structure = True @rule def tallest_building_when_completed(): for s in Structure: if s.building == True and s.height > 0.0 and s.completed > -1000000.0 and none(o in Structure: o.building == True and o.completed <= s.completed and o.until > s.completed and o.height > s.height): s.tallest_building = True @rule def undated_structure(): for s in Structure: if s.height > 0.0 and absent(s.completed): s.undated = True # ── Towers ─────────────────────────────────────────────────────────────────── class Tower: # A tower, by a name you choose: its height, its top's distance from the # vertical and its base's outer diameter, all in metres. `shift` is the # centre of mass's distance from the vertical on the model of a straight # tower of even weight, and `radius` the base's; `tips` is concluded where # the shift passes the radius. height: real offset: real base: real shift: real = offset / 2.0 radius: real = base / 2.0 tips: bool @rule def tower_tips(): for t in Tower: if t.shift > t.radius: t.tips = True # ── Hulls ──────────────────────────────────────────────────────────────────── class Hull: # A box hull, by a name you choose: its beam, its draught and the height of # its centre of gravity above the keel, in metres. `kb`, `bm` and `gm` are # computed; `unstable` is concluded where GM is not positive. `bm` is a # real for any beam and draught from 10^-150 to 10^150 metres; `gm` is # taken of halves and doubled, which changes no digit and keeps a sum past # the largest real from making it one. beam: real draught: real kg: real kb: real = draught / 2.0 bm: real = beam * beam / (12.0 * draught) gm: real = (0.5 * kb + 0.5 * bm - 0.5 * kg) * 2.0 unstable: bool @rule def unstable_hull(): for h in Hull: if h.gm <= 0.0: h.unstable = True # ── Rooms ──────────────────────────────────────────────────────────────────── class Room: # A room, by a name you choose: its volume in cubic metres and the lowest # and highest reverberation times you want, in seconds. `absorption` is the # sum of its surfaces' sabins, concluded by a rule, and `rt` is Sabine's # time over it. The rules conclude `too_live`, `too_dead` and `unmeasured`, # and `claims` and `surfaces`, which `disputed` compares: a surface bound # to the room twice, as two values of its area or coefficient bind it, # makes the room `disputed`, and then no absorption is concluded for it. volume: real low: real high: real claims: int surfaces: int disputed: bool absorption: real rt: real = 0.161 * volume / absorption too_live: bool too_dead: bool unmeasured: bool class Surface: # A surface lining a room, by a name you choose: the room's name, its area # in square metres and its absorption coefficient. `sabins` is computed. room: string area: real coefficient: real sabins: real = area * coefficient @rule def room_claims(): for r in Room, s in Surface: if s.room == r and s.sabins > -1.0e300: r.claims = count(s) @rule def room_surfaces(): for r in Room, s in Surface: if s.room == r and s.sabins > -1.0e300: r.surfaces = distinct(s) @rule def room_disputed(): # A sum adds every value it reads, so a surface bound to the room twice # would be counted under both values, which no survey gives. for r in Room: if r.claims > r.surfaces: r.disputed = True @rule def room_absorption(): for r in Room, s in Surface: if s.room == r and absent(r.disputed): r.absorption = sum(s.sabins) @rule def room_too_live(): for r in Room: if r.rt > r.high: r.too_live = True @rule def room_too_dead(): for r in Room: if r.rt < r.low: r.too_dead = True @rule def room_unmeasured(): for r in Room: if r.volume > 0.0 and none(s in Surface: s.room == r and s.area > -1.0): r.unmeasured = True # ── Channels ───────────────────────────────────────────────────────────────── class Fall: # A standard for a channel, by its name: the minimum gradient, a drop over # a length. Two sources that give it differently write two values. minimum: real class Channel: # A channel, by a name you choose: its length and its drop in the same # unit, and the name of the standard it is held to. `gradient` is computed; # the rules conclude `too_flat` and `no_standard`. length: real drop: real standard: string gradient: real = drop / length too_flat: bool no_standard: bool @rule def channel_too_flat(): for c in Channel, f in Fall: if c.standard == f and f.minimum > -1.0 and c.gradient < f.minimum: c.too_flat = True @rule def channel_without_standard(): for c in Channel: if none(f in Fall: c.standard == f and f.minimum > -1.0): c.no_standard = True # ── Writing facts ──────────────────────────────────────────────────────────── def building(name: string, features: list): # A building with the features it has, from "pointed_arch", "rib_vault", # "flying_buttress", "round_arch", "thick_walls", "barrel_vault", # "groin_vault", "dome" and "pendentives". All nine facts are written, True # for those named and False for the rest. Another name refuses. names = ["pointed_arch", "rib_vault", "flying_buttress", "round_arch", "thick_walls", "barrel_vault", "groin_vault", "dome", "pendentives"] for x in features: if x not in names: raise ValueError("a feature is one of pointed_arch, rib_vault, flying_buttress, round_arch, thick_walls, barrel_vault, groin_vault, dome, pendentives") update(Building(name).pointed_arch, "pointed_arch" in features, "building") update(Building(name).rib_vault, "rib_vault" in features, "building") update(Building(name).flying_buttress, "flying_buttress" in features, "building") update(Building(name).round_arch, "round_arch" in features, "building") update(Building(name).thick_walls, "thick_walls" in features, "building") update(Building(name).barrel_vault, "barrel_vault" in features, "building") update(Building(name).groin_vault, "groin_vault" in features, "building") update(Building(name).dome, "dome" in features, "building") update(Building(name).pendentives, "pendentives" in features, "building") def structure(name: string, height: float, completed: float, is_building: bool): # A structure with its height in metres, the year it was completed and # whether it is a building. It is written as standing; `fell()` records the # year it fell. Write it again to correct it. update(Structure(name).height, height, "structure") update(Structure(name).completed, completed, "structure") update(Structure(name).until, 10000.0, "structure") update(Structure(name).building, is_building, "structure") def fell(name: string, year: float): # The year a structure fell, or the part of it that made its height. update(Structure(name).until, year, "structure") def tower(name: string, height: float, offset: float, base: float): # A tower's height, its top's distance from the vertical and its base's # outer diameter, in metres. update(Tower(name).height, height, "tower") update(Tower(name).offset, offset, "tower") update(Tower(name).base, base, "tower") def hull(name: string, beam: float, draught: float, kg: float): # A box hull's beam, draught and height of its centre of gravity above the # keel, in metres. update(Hull(name).beam, beam, "hull") update(Hull(name).draught, draught, "hull") update(Hull(name).kg, kg, "hull") def room(name: string, volume: float, low: float, high: float): # A room's volume in cubic metres and the range of reverberation times # wanted, in seconds. update(Room(name).volume, volume, "room") update(Room(name).low, low, "room") update(Room(name).high, high, "room") def surface(name: string, room_name: string, area: float, coefficient: float): # A surface lining a room: its area in square metres and its absorption # coefficient. update(Surface(name).room, room_name, "surface") update(Surface(name).area, area, "surface") update(Surface(name).coefficient, coefficient, "surface") def fall(standard: string, minimum: float, source: string): # A standard's minimum gradient as a source gives it. A second source that # gives another value writes a second value of the same fact. update(Fall(standard).minimum, minimum, source) def vitruvius(): # Vitruvius's minimum fall for a channel, under the name "vitruvius", as # the two sources read give it: half a foot in a hundred, and one in 4,800. fall("vitruvius", 0.5 / 100.0, "half-a-foot-in-a-hundred") fall("vitruvius", 1.0 / 4800.0, "one-in-4800") def channel(name: string, length: float, drop: float, standard: string): # A channel's length and drop in the same unit, and the standard it is held # to. update(Channel(name).length, length, "channel") update(Channel(name).drop, drop, "channel") update(Channel(name).standard, standard, "channel") # ── Reading what the rules concluded ───────────────────────────────────────── # # Each reader reads every value in force, so a fact two sources disagree # about gives each of its values a verdict, and `unsettled()` names the facts # that hold two. def names_of(values: claim_list) -> list: out = [] for v in values: parts = v.path.split(".") x = parts[len(parts) - 2] if v.value == True and x not in out: out = out + [x] return sorted(out) def styles_of(name: string) -> list: # The styles and forms the rules conclude for a building, in the order # gothic, romanesque, pendentive_dome. out = [] va = list_values(Building.gothic) vb = list_values(Building.romanesque) vc = list_values(Building.pendentive_dome) if name in names_of(va): out = out + ["gothic"] if name in names_of(vb): out = out + ["romanesque"] if name in names_of(vc): out = out + ["pendentive_dome"] return out def tallest_structures() -> list: # The structures that were the tallest when they were completed, sorted. vs = list_values(Structure.tallest_structure) return names_of(vs) def tallest_buildings() -> list: # The buildings that were the tallest building when they were completed, # sorted. vs = list_values(Structure.tallest_building) return names_of(vs) def undated_structures() -> list: # The structures with a height and no completion year, sorted. They are # not ranked. vs = list_values(Structure.undated) return names_of(vs) def reals(values: claim_list) -> dict: # Each subject's number. A subject that holds two different values refuses, # since no one of them is the answer. out = {} for v in values: parts = v.path.split(".") s = parts[len(parts) - 2] match v.value: case real as x: if s in out and out[s] != x: raise ValueError(s + " holds two values for " + parts[len(parts) - 1] + ", and unsettled() names it") out[s] = x case default: raise ValueError("a record holds a value that is not a number") return out def record_until(name: string, buildings_only: bool) -> float: # The year a record passed: the earlier of the year the structure fell and # the year a taller one was completed, over all structures, or over # buildings when `buildings_only` is True. It returns 10000.0 when no # structure you recorded passed it and it has not fallen: passed by nothing # recorded, not held forever. A structure that was not the tallest when it # was completed refuses, and so does a structure's height, year of # completion, year of falling or building flag that holds two values, since # any of them can move the year; `unsettled()` names the fact. if buildings_only: vt = list_values(Structure.tallest_building) else: vt = list_values(Structure.tallest_structure) if name not in names_of(vt): raise ValueError(name + " was not the tallest when it was completed") hs = list_values(Structure.height) cs = list_values(Structure.completed) us = list_values(Structure.until) bs = list_values(Structure.building) height = reals(hs) done = reals(cs) until = reals(us) isb = {} for v in bs: parts = v.path.split(".") match v.value: case bool as b: s = parts[len(parts) - 2] if s in isb and isb[s] != b: raise ValueError(s + " holds two values for building, and unsettled() names it") isb[s] = b case default: raise ValueError("a record's building flag is not True or False") end = until[name] for o in done: if o != name and o in height and done[o] > done[name] and height[o] > height[name] and done[o] < end: if buildings_only == False or (o in isb and isb[o] == True): end = done[o] return end def tipping_towers() -> list: # The towers whose centre of mass passes the edge of their base, sorted. vs = list_values(Tower.tips) return names_of(vs) def unstable_hulls() -> list: # The hulls whose metacentric height is not positive, sorted. vs = list_values(Hull.unstable) return names_of(vs) def live_rooms() -> list: # The rooms that ring longer than their range, sorted. vs = list_values(Room.too_live) return names_of(vs) def dead_rooms() -> list: # The rooms that ring shorter than their range, sorted. vs = list_values(Room.too_dead) return names_of(vs) def unmeasured_rooms() -> list: # The rooms with a volume and no surface recorded, sorted. vs = list_values(Room.unmeasured) return names_of(vs) def disputed_rooms() -> list: # The rooms with a surface bound to them twice, whose absorption, time and # verdicts wait until one of its values is forgotten, sorted. vs = list_values(Room.disputed) return names_of(vs) def flat_channels() -> list: # The channels whose gradient is below their standard's minimum under at # least one of its values, sorted. vs = list_values(Channel.too_flat) return names_of(vs) def channels_without_standard() -> list: # The channels whose standard nobody wrote, sorted. vs = list_values(Channel.no_standard) return names_of(vs) def disputes(values: claim_list, noun: string) -> list: out = [] seen = {} for v in values: parts = v.path.split(".") s = noun + " " + parts[len(parts) - 2] + " " + parts[len(parts) - 1] x = str(v.value) if s in seen and seen[s] != x and s not in out: out = out + [s] seen[s] = x return out def unsettled() -> list: # Each fact you write that holds two different values, as "building x # dome", "structure x height", "fall vitruvius minimum" and so on, sorted. # A fact is named only when its values differ. out = [] v1 = list_values(Building.pointed_arch) out = out + disputes(v1, "building") v2 = list_values(Building.rib_vault) out = out + disputes(v2, "building") v3 = list_values(Building.flying_buttress) out = out + disputes(v3, "building") v4 = list_values(Building.round_arch) out = out + disputes(v4, "building") v5 = list_values(Building.thick_walls) out = out + disputes(v5, "building") v6 = list_values(Building.barrel_vault) out = out + disputes(v6, "building") v7 = list_values(Building.groin_vault) out = out + disputes(v7, "building") v8 = list_values(Building.dome) out = out + disputes(v8, "building") v9 = list_values(Building.pendentives) out = out + disputes(v9, "building") v10 = list_values(Structure.height) out = out + disputes(v10, "structure") v11 = list_values(Structure.completed) out = out + disputes(v11, "structure") v12 = list_values(Structure.until) out = out + disputes(v12, "structure") v13 = list_values(Structure.building) out = out + disputes(v13, "structure") v14 = list_values(Tower.height) out = out + disputes(v14, "tower") v15 = list_values(Tower.offset) out = out + disputes(v15, "tower") v16 = list_values(Tower.base) out = out + disputes(v16, "tower") v17 = list_values(Hull.beam) out = out + disputes(v17, "hull") v18 = list_values(Hull.draught) out = out + disputes(v18, "hull") v19 = list_values(Hull.kg) out = out + disputes(v19, "hull") v20 = list_values(Room.volume) out = out + disputes(v20, "room") v21 = list_values(Room.low) out = out + disputes(v21, "room") v22 = list_values(Room.high) out = out + disputes(v22, "room") v23 = list_values(Surface.room) out = out + disputes(v23, "surface") v24 = list_values(Surface.area) out = out + disputes(v24, "surface") v25 = list_values(Surface.coefficient) out = out + disputes(v25, "surface") v26 = list_values(Fall.minimum) out = out + disputes(v26, "fall") v27 = list_values(Channel.length) out = out + disputes(v27, "channel") v28 = list_values(Channel.drop) out = out + disputes(v28, "channel") v29 = list_values(Channel.standard) out = out + disputes(v29, "channel") return sorted(out) # ── Calculation ────────────────────────────────────────────────────────────── def lean_degrees(offset: float, height: float) -> float: # A lean in degrees from the top's distance from the vertical and the # height: degrees(atan(offset / height)). return math.degrees(math.atan(offset / height)) def top_offset(height: float, degrees: float) -> float: # The top's distance from the vertical for a height and a lean in degrees: # height x tan(radians(degrees)). The lean is taken modulo 360 degrees # first, which math.fmod does exactly, so a turn's worth of degrees loses # no digits to radians; where the product is not a normal real, it is # worked on the mantissas and the powers of two apart, as math.frexp gives # them. t = math.tan(math.radians(math.fmod(degrees, 360.0))) v = height * t if (math.isfinite(v) and abs(v) >= 2.2250738585072014e-308) or height == 0.0 or t == 0.0: return v a = math.frexp(height) b = math.frexp(t) return math.ldexp(a[0] * b[0], a[1] + b[1]) def reverberation_time(volume: float, absorption: float) -> float: # Sabine's reverberation time in seconds: 0.161 x V / A, V in cubic metres # and A in sabins (square metres of absorption). return 0.161 * volume / absorption def metacentric_height(beam: float, draught: float, kg: float) -> float: # A box hull's metacentric height in metres: draught / 2 + beam² / (12 x # draught) - KG, in the order the computed fields take, so it is the field # `gm` to the last digit. Where beam² / (12 x draught) is not a normal real, # its half is worked on the mantissas and the powers of two apart, as # math.frexp gives them, so the height is had for any beam and draught. kb = draught / 2.0 bm = beam * beam / (12.0 * draught) half = 0.5 * bm if not (math.isfinite(bm) and abs(bm) >= 2.2250738585072014e-308) and beam != 0.0: a = math.frexp(beam) d = math.frexp(draught) half = math.ldexp(a[0] * a[0] / (12.0 * d[0]), 2 * a[1] - d[1] - 1) return (0.5 * kb + half - 0.5 * kg) * 2.0 def gradient(length: float, drop: float) -> float: # A channel's gradient, drop / length. return drop / length def one_in(length: float, drop: float) -> float: # A gradient as one in N: length / drop. return length / drop def pendulum_period(length: float) -> float: # A pendulum's period in seconds for a length in metres, at standard # gravity, for a small swing: 2 pi sqrt(L / 9.80665). return 2.0 * math.pi * math.sqrt(length / 9.80665) def pendulum_length(period: float) -> float: # The length in metres of a pendulum with a period in seconds, at standard # gravity, for a small swing: 9.80665 x (T / 2 pi)². return 9.80665 * (period / (2.0 * math.pi)) ** 2