Module: Causalontology::Semantics
- Defined in:
- lib/causalontology/semantics.rb
Constant Summary collapse
- UNIT_SECONDS =
Rule 4: the fixed unit-conversion constants (average Gregorian values).
{ "instant" => 0, "seconds" => 1, "minutes" => 60, "hours" => 3600, "days" => 86400, "weeks" => 604800, "months" => 2629746, "years" => 31556952, }.freeze
- ENRICHMENT_FIELDS =
Rule 12: enrichment field-to-kind validity and entry shapes. Two occurrent forms added in 2.0.0.
{ "aliases" => [["occurrent", "continuant"], "alias"], "participants" => [["occurrent"], "continuant"], "subsumes" => [["continuant"], "continuant"], "part_of" => [["continuant"], "continuant"], "realized_in" => [["realizable"], "occurrent"], "occurrent_subsumes" => [["occurrent"], "occurrent"], "occurrent_part_of" => [["occurrent"], "occurrent"], }.freeze
- CRO_OPTIONAL_FIELDS =
["mechanism", "temporal", "modality", "context"].freeze
- POSITIVE =
Rule 6 (amended): necessary, sufficient, contributory, enabling are mutually compatible; preventive opposes all four.
Set.new(["necessary", "sufficient", "contributory", "enabling"]).freeze
Class Method Summary collapse
-
.admissible(cro, elapsed_seconds) ⇒ Object
Rule 4: temporal admissibility with the fixed constants.
-
.bridge_closure(occurrent_id, bridges) ⇒ Object
ALGORITHM A.
-
.bridge_wellformed(bridge, occ_map, stratum_map) ⇒ Object
Rule 14 / N3.2.1: Bridge well-formedness.
-
.classify_cro(cro, occ_map, stratum_map) ⇒ Object
ALGORITHM C (Rule 15): "intra_stratal" | "adjacent_stratal" | "skipping" | "mixed" | "unclassifiable" | "scheme_mismatch".
-
.conduit_wellformed(conduit, port_map, cro_map = nil) ⇒ Object
Rule 17 / N4.2.1-2: Conduit well-formedness.
-
.conflicts(a, b) ⇒ Object
Rule 6: the formal conflict test.
- .contexts_compatible(a, b) ⇒ Object
-
.covering_law_mismatch(tcc, token_map, law) ⇒ Object
Rule 20: true iff the token claim's cause/effect tokens do not instantiate the covering law's causes/effects (surfaces covering_law_mismatch).
-
.delay_within_window(actual_delay, temporal) ⇒ Object
ALGORITHM E (Rule 20): does an observed delay fall within a covering law's temporal window? Inclusive at both ends (N12.5.2).
-
.endpoints_mixed(cro, occ_map) ⇒ Object
True iff causes or effects span more than one distinct stratum (surfaces mixed_stratal_endpoints, an invitation; N12.3.2).
-
.has_cycle(edges) ⇒ Object
Rules 4 / 6.1: true iff a directed graph (node -> iterable of successors) has a cycle.
-
.hierarchy_consistent(parent, members, bridges = []) ⇒ Object
ALGORITHM B (amended Rule 7): "consistent" | "inconsistent" | "indeterminate", ACROSS STRATA via bridged reachability.
-
.is_partial(cro) ⇒ Object
[partial, missing] - which optional CRO fields are unspecified.
- .kind_of_id(identifier) ⇒ Object
- .path_exists(edges, src, dst) ⇒ Object
-
.refinement_valid(child, parent) ⇒ Object
Rule 3: [ok, reason] - is child a valid refinement of parent?.
-
.retrocausal(tcc, token_map) ⇒ Object
Rule 21: true iff any cause token starts after any effect token (HARD; retrocausal_claim).
-
.skip_gaps(cro, classification) ⇒ Object
ALGORITHM D (Rule 16): the gaps a Causal Relation Object surfaces for the skip decision.
-
.state_gaps(state, quality) ⇒ Object
Rule 19 / N5.3.1-2: the HARD gaps a state assertion surfaces against its quality: value_type_mismatch and/or unit_mismatch.
-
.to_seconds(duration, unit) ⇒ Object
ALGORITHM E helper: normalize a delay to seconds by the fixed table.
-
.validate_semantics(obj, kind = nil) ⇒ Object
[ok, reasons] - the locally checkable semantic rules.
- .window_overlap(a, b) ⇒ Object
Class Method Details
.admissible(cro, elapsed_seconds) ⇒ Object
Rule 4: temporal admissibility with the fixed constants.
109 110 111 112 113 114 115 116 |
# File 'lib/causalontology/semantics.rb', line 109 def admissible(cro, elapsed_seconds) t = cro["temporal"] return true if t.nil? # no window imposes no constraint unit = UNIT_SECONDS.fetch(t["unit"]) lo = t["minimum_delay"] * unit hi = t["maximum_delay"] * unit lo <= elapsed_seconds && elapsed_seconds <= hi end |
.bridge_closure(occurrent_id, bridges) ⇒ Object
ALGORITHM A. Every finer occurrent an occurrent resolves to, following
Bridges downward, transitively. Includes the starting occurrent
(N12.1.1). bridges is any iterable of bridge objects. The visited guard
(N12.1.2) prevents an infinite loop on malformed cyclic data.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/causalontology/semantics.rb', line 186 def bridge_closure(occurrent_id, bridges) result = Set.new([occurrent_id]) frontier = [occurrent_id] visited = Set.new coarse_index = {} bridges.each { |b| (coarse_index[b["coarse"]] ||= []) << b } until frontier.empty? current = frontier.pop next if visited.include?(current) visited << current (coarse_index[current] || []).each do |b| b["fine"].each do |f| result << f frontier << f end end end result end |
.bridge_wellformed(bridge, occ_map, stratum_map) ⇒ Object
Rule 14 / N3.2.1: Bridge well-formedness. [ok, reason]. All of (a)-(e) of N3.2.1 must hold, else malformed_bridge.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/causalontology/semantics.rb', line 330 def bridge_wellformed(bridge, occ_map, stratum_map) coarse = occ_map[bridge["coarse"]] || {} cs = coarse["stratum"] return [false, "malformed_bridge: coarse has no stratum (a)"] if cs.nil? fine_strata = bridge["fine"].map { |f| (occ_map[f] || {})["stratum"] } if fine_strata.any?(&:nil?) return [false, "malformed_bridge: a fine member has no stratum (b)"] end if Set.new(fine_strata).length != 1 return [false, "malformed_bridge: fine members span >1 stratum (c)"] end fs = fine_strata[0] if stratum_map[cs]["scheme"] != stratum_map[fs]["scheme"] return [false, "malformed_bridge: coarse and fine differ in scheme (d)"] end unless stratum_map[cs]["ordinal"] > stratum_map[fs]["ordinal"] return [false, "malformed_bridge: coarse ordinal not > fine ordinal (e)"] end [true, "well-formed bridge"] end |
.classify_cro(cro, occ_map, stratum_map) ⇒ Object
ALGORITHM C (Rule 15): "intra_stratal" | "adjacent_stratal" | "skipping" | "mixed" | "unclassifiable" | "scheme_mismatch". Derived, never asserted; recompute on ingest (N12.3.1).
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/causalontology/semantics.rb', line 254 def classify_cro(cro, occ_map, stratum_map) stratum_of = lambda { |occ_id| (occ_map[occ_id] || {})["stratum"] } cause_strata = cro["causes"].map { |c| stratum_of.call(c) } effect_strata = cro["effects"].map { |e| stratum_of.call(e) } if (cause_strata + effect_strata).any?(&:nil?) return "unclassifiable" # surface unstratified_occurrent (invitation) end all_strata = Set.new(cause_strata) | Set.new(effect_strata) schemes = Set.new(all_strata.map { |s| stratum_map[s]["scheme"] }) return "scheme_mismatch" if schemes.length > 1 # HARD c_ord = cause_strata.map { |s| stratum_map[s]["ordinal"] } e_ord = effect_strata.map { |s| stratum_map[s]["ordinal"] } if c_ord.max == c_ord.min && c_ord.min == e_ord.max && e_ord.max == e_ord.min return "intra_stratal" end pairs = c_ord.product(e_ord).map { |i, j| (i - j).abs } gap = pairs.min span = pairs.max return "adjacent_stratal" if span == 1 return "skipping" if gap > 1 "mixed" # some pairs adjacent, some skipping end |
.conduit_wellformed(conduit, port_map, cro_map = nil) ⇒ Object
Rule 17 / N4.2.1-2: Conduit well-formedness. [ok, reason]. N4.2.1 with the transform exception of N4.2.2.
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/causalontology/semantics.rb', line 353 def conduit_wellformed(conduit, port_map, cro_map = nil) frm = port_map[conduit["from"]] to = port_map[conduit["to"]] if frm.nil? || to.nil? return [false, "malformed_conduit: dangling port reference"] end unless ["out", "bidirectional"].include?(frm["direction"]) return [false, "malformed_conduit: from port is not out/bidirectional (a)"] end unless ["in", "bidirectional"].include?(to["direction"]) return [false, "malformed_conduit: to port is not in/bidirectional (b)"] end carries = conduit["carries"] unless carries.all? { |o| frm["accepts"].include?(o) } return [false, "malformed_conduit: carries not accepted by from (c)"] end transform = conduit["transform"] if transform.nil? unless carries.all? { |o| to["accepts"].include?(o) } return [false, "malformed_conduit: carries not accepted by to (d)"] end else law = (cro_map || {})[transform] unless law.nil? unless law["effects"].all? { |o| to["accepts"].include?(o) } return [false, "malformed_conduit: transform effects not " \ "accepted by to (d, relaxed per N4.2.2)"] end end end [true, "well-formed conduit"] end |
.conflicts(a, b) ⇒ Object
Rule 6: the formal conflict test.
141 142 143 144 145 146 147 148 149 150 |
# File 'lib/causalontology/semantics.rb', line 141 def conflicts(a, b) return false if Set.new(a["causes"]) != Set.new(b["causes"]) return false if Set.new(a["effects"]) != Set.new(b["effects"]) return false unless contexts_compatible(a, b) return false unless window_overlap(a, b) ma = a["modality"] mb = b["modality"] (ma == "preventive" && POSITIVE.include?(mb)) || (mb == "preventive" && POSITIVE.include?(ma)) end |
.contexts_compatible(a, b) ⇒ Object
131 132 133 134 135 136 137 138 |
# File 'lib/causalontology/semantics.rb', line 131 def contexts_compatible(a, b) ca = a["context"] cb = b["context"] return true if ca.nil? || ca.empty? || cb.nil? || cb.empty? sa = Set.new(ca) sb = Set.new(cb) sa == sb || sa.subset?(sb) || sb.subset?(sa) end |
.covering_law_mismatch(tcc, token_map, law) ⇒ Object
Rule 20: true iff the token claim's cause/effect tokens do not instantiate the covering law's causes/effects (surfaces covering_law_mismatch).
407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/causalontology/semantics.rb', line 407 def covering_law_mismatch(tcc, token_map, law) return false if law.nil? law_causes = Set.new(law["causes"]) law_effects = Set.new(law["effects"]) tcc["causes"].each do |c| return true unless law_causes.include?(token_map[c]["instantiates"]) end tcc["effects"].each do |e| return true unless law_effects.include?(token_map[e]["instantiates"]) end false end |
.delay_within_window(actual_delay, temporal) ⇒ Object
ALGORITHM E (Rule 20): does an observed delay fall within a covering law's temporal window? Inclusive at both ends (N12.5.2).
319 320 321 322 323 324 325 326 |
# File 'lib/causalontology/semantics.rb', line 319 def delay_within_window(actual_delay, temporal) return true if actual_delay.nil? || actual_delay.empty? || temporal.nil? || temporal.empty? observed = to_seconds(actual_delay["duration"], actual_delay["unit"]) lo = to_seconds(temporal["minimum_delay"], temporal["unit"]) hi = to_seconds(temporal["maximum_delay"], temporal["unit"]) lo <= observed && observed <= hi end |
.endpoints_mixed(cro, occ_map) ⇒ Object
True iff causes or effects span more than one distinct stratum (surfaces mixed_stratal_endpoints, an invitation; N12.3.2).
279 280 281 282 283 284 285 |
# File 'lib/causalontology/semantics.rb', line 279 def endpoints_mixed(cro, occ_map) stratum_of = lambda { |occ_id| (occ_map[occ_id] || {})["stratum"] } cs = Set.new(cro["causes"].map { |c| stratum_of.call(c) }) es = Set.new(cro["effects"].map { |e| stratum_of.call(e) }) return false if cs.include?(nil) || es.include?(nil) cs.length > 1 || es.length > 1 end |
.has_cycle(edges) ⇒ Object
Rules 4 / 6.1: true iff a directed graph (node -> iterable of successors) has a cycle. Used for bridge graph, occurrent_subsumes, occurrent_part_of, and token mereology (part_of).
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'lib/causalontology/semantics.rb', line 436 def has_cycle(edges) white = 0 grey = 1 black = 2 state = {} visit = nil visit = lambda do |node| state[node] = grey (edges[node] || []).each do |nxt| s = state.fetch(nxt, white) return true if s == grey return true if s == white && visit.call(nxt) end state[node] = black false end edges.keys.any? { |n| state.fetch(n, white) == white && visit.call(n) } end |
.hierarchy_consistent(parent, members, bridges = []) ⇒ Object
ALGORITHM B (amended Rule 7): "consistent" | "inconsistent" | "indeterminate", ACROSS STRATA via bridged reachability.
members: mapping from CRO identifier to CRO object for the mechanism entries. bridges: the store's bridges (empty -> 1.0.0 literal reachability, the degenerate case, N12.2.3).
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/causalontology/semantics.rb', line 225 def hierarchy_consistent(parent, members, bridges = []) mechanism = parent["mechanism"] || [] return "consistent" if mechanism.empty? # nothing claimed (N12.2.1) edges = {} mechanism.each do |mid| m = members[mid] return "indeterminate" if m.nil? # dangling; ignorance, not refutation m["causes"].each do |c| (edges[c] ||= Set.new).merge(m["effects"]) end end b_cause = {} parent["causes"].each { |c| b_cause[c] = bridge_closure(c, bridges) } b_effect = {} parent["effects"].each { |e| b_effect[e] = bridge_closure(e, bridges) } parent["causes"].each do |c| parent["effects"].each do |e| connected = b_cause[c].any? do |cp| b_effect[e].any? { |ep| path_exists(edges, cp, ep) } end return "inconsistent" unless connected end end "consistent" end |
.is_partial(cro) ⇒ Object
[partial, missing] - which optional CRO fields are unspecified.
103 104 105 106 |
# File 'lib/causalontology/semantics.rb', line 103 def is_partial(cro) missing = CRO_OPTIONAL_FIELDS.reject { |f| cro.key?(f) } [!missing.empty?, missing] end |
.kind_of_id(identifier) ⇒ Object
46 47 48 |
# File 'lib/causalontology/semantics.rb', line 46 def kind_of_id(identifier) Canonical::KIND_OF_PREFIX[identifier.split(":", 2)[0]] end |
.path_exists(edges, src, dst) ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/causalontology/semantics.rb', line 206 def path_exists(edges, src, dst) seen = Set.new stack = [src] until stack.empty? node = stack.pop return true if node == dst next if seen.include?(node) seen << node stack.concat((edges[node] || []).to_a) end false end |
.refinement_valid(child, parent) ⇒ Object
Rule 3: [ok, reason] - is child a valid refinement of parent?
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/causalontology/semantics.rb', line 153 def refinement_valid(child, parent) if child["refines"] != parent["id"] return [false, "child does not name the parent in refines"] end if Set.new(child["causes"]) != Set.new(parent["causes"]) || Set.new(child["effects"]) != Set.new(parent["effects"]) return [false, "a refinement must keep the parent's causes and effects"] end added = 0 CRO_OPTIONAL_FIELDS.each do |field| if parent.key?(field) if child[field] != parent[field] return [false, "a refinement may not change a field the " \ "parent specified; this is a rival claim"] end elsif child.key?(field) added += 1 end end if added == 0 return [false, "a refinement must add at least one unspecified field"] end [true, "valid refinement"] end |
.retrocausal(tcc, token_map) ⇒ Object
Rule 21: true iff any cause token starts after any effect token (HARD; retrocausal_claim). RFC 3339 UTC "Z" strings compare lexicographically.
422 423 424 425 426 427 428 429 430 431 |
# File 'lib/causalontology/semantics.rb', line 422 def retrocausal(tcc, token_map) tcc["causes"].each do |c| cstart = token_map[c]["interval"]["start"] tcc["effects"].each do |e| estart = token_map[e]["interval"]["start"] return true if cstart > estart end end false end |
.skip_gaps(cro, classification) ⇒ Object
ALGORITHM D (Rule 16): the gaps a Causal Relation Object surfaces for the skip decision. THE ASYMMETRY (clause 3) is the whole point of the field and is implemented exactly.
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/causalontology/semantics.rb', line 290 def skip_gaps(cro, classification) gaps = [] has_mech = !(cro["mechanism"] || []).empty? if cro["skips"] == true && has_mech gaps << "contradictory_skip" # HARD return gaps end if cro["skips"] == true && !["skipping", "unclassifiable"].include?(classification) gaps << "vacuous_skip" # invitation end if classification == "skipping" && !has_mech if cro["skips"] == true # NOTHING: absence is a finding else gaps << "incomplete_mechanism" # invitation end end gaps end |
.state_gaps(state, quality) ⇒ Object
Rule 19 / N5.3.1-2: the HARD gaps a state assertion surfaces against its quality: value_type_mismatch and/or unit_mismatch.
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/causalontology/semantics.rb', line 388 def state_gaps(state, quality) gaps = [] dt = quality["datatype"] v = state["value"] || {} shape = if v.key?("quantity") then "quantity" elsif v.key?("categorical") then "categorical" elsif v.key?("boolean") then "boolean" end if shape != dt gaps << "value_type_mismatch" elsif dt == "quantity" && v["unit"] != quality["unit"] gaps << "unit_mismatch" end gaps end |
.to_seconds(duration, unit) ⇒ Object
ALGORITHM E helper: normalize a delay to seconds by the fixed table.
312 313 314 315 |
# File 'lib/causalontology/semantics.rb', line 312 def to_seconds(duration, unit) return 0 if unit == "instant" duration * UNIT_SECONDS.fetch(unit) end |
.validate_semantics(obj, kind = nil) ⇒ Object
[ok, reasons] - the locally checkable semantic rules.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/causalontology/semantics.rb', line 51 def validate_semantics(obj, kind = nil) kind ||= Canonical.infer_kind(obj) errors = [] if kind == "causal_relation_object" t = obj["temporal"] if !t.nil? && !t["minimum_delay"].nil? && !t["maximum_delay"].nil? && t["minimum_delay"] > t["maximum_delay"] errors << "minimum_delay must be <= maximum_delay" end oid = obj["id"] if oid && (obj["mechanism"] || []).include?(oid) errors << "mechanism must be acyclic " \ "(a Causal Relation Object may not contain itself)" end if oid && obj["refines"] == oid errors << "refines must be acyclic" end # Rule 16, clause 1 (contradictory_skip): a HARD, locally-decidable # contradiction between skips:true and a non-empty mechanism. if obj["skips"] == true && obj["mechanism"] && !obj["mechanism"].empty? errors << "contradictory_skip: skips is true but a mechanism " \ "is present" end end if kind == "enrichment" field = obj["field"] about = obj["about"] || "" entry = obj["entry"] spec = ENRICHMENT_FIELDS[field] if spec legal_kinds, shape = spec about_kind = kind_of_id(about) if about_kind && !legal_kinds.include?(about_kind) errors << "#{field} is not a legal field for a #{about_kind} (rule 12)" end if shape == "alias" unless entry.is_a?(Hash) && entry.key?("lang") && entry.key?("text") errors << "an aliases entry must be a language-tagged text object" end else unless entry.is_a?(String) && entry.start_with?(shape + ":") errors << "a #{field} entry must be a #{shape}: identifier" end end end end [errors.empty?, errors] end |
.window_overlap(a, b) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/causalontology/semantics.rb', line 118 def window_overlap(a, b) ta = a["temporal"] tb = b["temporal"] return true if ta.nil? || tb.nil? # either absent counts as overlapping ua = UNIT_SECONDS.fetch(ta["unit"]) ub = UNIT_SECONDS.fetch(tb["unit"]) lo_a = ta["minimum_delay"] * ua hi_a = ta["maximum_delay"] * ua lo_b = tb["minimum_delay"] * ub hi_b = tb["maximum_delay"] * ub lo_a <= hi_b && lo_b <= hi_a end |