<?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: Matheus de Camargo Marques</title>
    <description>The latest articles on DEV Community by Matheus de Camargo Marques (@matheuscamarques).</description>
    <link>https://dev.to/matheuscamarques</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F882243%2Fde69153a-a393-4f28-9761-e5b60402c45b.jpeg</url>
      <title>DEV Community: Matheus de Camargo Marques</title>
      <link>https://dev.to/matheuscamarques</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/matheuscamarques"/>
    <language>en</language>
    <item>
      <title>Bibliography — PON + Smart Brewery dev.to series (EN drafts)</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:28:41 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9</link>
      <guid>https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9</guid>
      <description>&lt;h2&gt;
  
  
  Bibliography — PON + Smart Brewery dev.to series (EN drafts)
&lt;/h2&gt;

&lt;p&gt;Normalized references used across &lt;a href="https://dev.toen/"&gt;&lt;code&gt;docs/devto/en/&lt;/code&gt;&lt;/a&gt;. When publishing on dev.to, prefer stable URLs; verify DOIs and publisher pages periodically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paradigms and architecture
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simão, J. M.; Borges, M. R.; Ebina, R.; Tacla, C. A.; Stadzisz, P. C.; Banaszewski, R. F.&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Notification Oriented Paradigm (NOP) and Imperative Paradigm: A Comparative Study&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.scirp.org/journal/paperinformation?paperid=19842" rel="noopener noreferrer"&gt;SCIRP / IJSEA&lt;/a&gt; — academic treatment of NOP (closely related to “notification-oriented” rule structuring; this repo uses &lt;strong&gt;PON&lt;/strong&gt; as shorthand).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cockburn, A.&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Hexagonal architecture&lt;/em&gt; (ports and adapters)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://alistair.cockburn.us/hexagonal-architecture/" rel="noopener noreferrer"&gt;alistair.cockburn.us&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fowler, M.&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Ports and Adapters / Hexagonal architecture&lt;/em&gt; (summary)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://martinfowler.com/bliki/HexagonalArchitecture.html" rel="noopener noreferrer"&gt;martinfowler.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Erlang, Elixir, OTP
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Armstrong, J.&lt;/td&gt;
&lt;td&gt;2003&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Making reliable distributed systems in the presence of software errors&lt;/em&gt; (PhD thesis)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.erlang.org/download/armstrong_thesis_2003.pdf" rel="noopener noreferrer"&gt;erlang.org&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cesarini, F.; Thompson, S.&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Programming Erlang (2nd ed.)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Pragmatic Bookshelf — OTP design, processes, supervision.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Elixir documentation&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GenServer&lt;/code&gt;, &lt;code&gt;Registry&lt;/code&gt;, &lt;code&gt;Supervisor&lt;/code&gt;, &lt;code&gt;Macro&lt;/code&gt;, &lt;code&gt;@behaviour&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hexdocs.pm/elixir/" rel="noopener noreferrer"&gt;hexdocs.pm/elixir&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Mix profiling tasks&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mix profile.cprof&lt;/code&gt;, &lt;code&gt;eprof&lt;/code&gt;, &lt;code&gt;fprof&lt;/code&gt;, &lt;code&gt;tprof&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://hexdocs.pm/mix/Mix.Tasks.Profile.Cprof.html" rel="noopener noreferrer"&gt;hexdocs.pm/mix/Mix.Tasks.Profile.Cprof.html&lt;/a&gt; and sibling task modules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Phoenix ecosystem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Phoenix LiveView&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Guides and API&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hexdocs.pm/phoenix_live_view/" rel="noopener noreferrer"&gt;hexdocs.pm/phoenix_live_view&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Phoenix PubSub&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hexdocs.pm/phoenix_pubsub/" rel="noopener noreferrer"&gt;hexdocs.pm/phoenix_pubsub&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Data pipelines and observability
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Broadway&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hexdocs.pm/broadway/" rel="noopener noreferrer"&gt;hexdocs.pm/broadway&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;GenStage&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hexdocs.pm/gen_stage/" rel="noopener noreferrer"&gt;hexdocs.pm/gen_stage&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Telemetry&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;telemetry&lt;/code&gt; for Erlang/Elixir&lt;/td&gt;
&lt;td&gt;&lt;a href="https://hexdocs.pm/telemetry/" rel="noopener noreferrer"&gt;hexdocs.pm/telemetry&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timescale, Inc.&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;TimescaleDB documentation&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.timescale.com/" rel="noopener noreferrer"&gt;docs.timescale.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Analytics and BI
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kimball, R.; Ross, M.&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;The Data Warehouse Toolkit (3rd ed.)&lt;/em&gt; — dimensional modeling&lt;/td&gt;
&lt;td&gt;Wiley — star schema, facts/dimensions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Power BI REST APIs (push datasets, etc.)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://learn.microsoft.com/en-us/rest/api/power-bi/" rel="noopener noreferrer"&gt;Microsoft Learn — Power BI REST&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL Global Development Group&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GRANT&lt;/code&gt;, roles, row security&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html" rel="noopener noreferrer"&gt;postgresql.org/docs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Digital twins and industrial context
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grieves, M.&lt;/td&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Digital Twin: Manufacturing Excellence Through Virtual Factory Replication&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Often cited white paper on digital-twin vocabulary (search publisher copy; ISBN/institutional PDFs vary).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Machine learning lifecycle
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breck, E.; et al.&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;&lt;em&gt;The ML Test Score: A Rubric for ML Production Readiness and Technical Debt Reduction&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://research.google/pubs/pub46555/" rel="noopener noreferrer"&gt;research.google&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Elixir Numerics&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Axon, Scholar (Neural Network / traditional ML in Elixir)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://hexdocs.pm/axon/" rel="noopener noreferrer"&gt;hexdocs.pm/axon&lt;/a&gt;, &lt;a href="https://hexdocs.pm/scholar/" rel="noopener noreferrer"&gt;hexdocs.pm/scholar&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Queues, performance, back-pressure
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author(s)&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Little, J. D. C.&lt;/td&gt;
&lt;td&gt;1961&lt;/td&gt;
&lt;td&gt;&lt;em&gt;A Proof for the Queuing Formula L = λW&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Operations Research&lt;/em&gt; — foundation for &lt;strong&gt;Little’s Law&lt;/strong&gt; (relation of queue length, arrival rate, wait); &lt;a href="https://en.wikipedia.org/wiki/Little%27s_law" rel="noopener noreferrer"&gt;Wikipedia summary&lt;/a&gt; with citation to original.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Erlang docs&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;:erlang.process_info/2&lt;/code&gt; (mailbox size, etc.)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.erlang.org/doc/man/erlang.html" rel="noopener noreferrer"&gt;erlang.org/doc/man/erlang.html&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  This repository (deep dives in Portuguese or internal)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance, profilers, env matrix&lt;/td&gt;
&lt;td&gt;
&lt;a href="//../performance-dev.md"&gt;&lt;code&gt;docs/performance-dev.md&lt;/code&gt;&lt;/a&gt; — EN series walkthrough: &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;Part 11 on dev.to — Dev profiling…&lt;/a&gt; · &lt;a href="//en/11_dev_profiling_cpu_memory_optimizations.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory pressure heuristics&lt;/td&gt;
&lt;td&gt;&lt;a href="//../memory-pressure-heuristics.md"&gt;&lt;code&gt;docs/memory-pressure-heuristics.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message storm mitigation&lt;/td&gt;
&lt;td&gt;PT: &lt;a href="//../artigos/19_mitigacao_message_storm_pon_elixir_smart_brewery.md"&gt;&lt;code&gt;docs/artigos/19_mitigacao_message_storm_pon_elixir_smart_brewery.md&lt;/code&gt;&lt;/a&gt; — EN series: &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;Part 10 on dev.to — When notifications explode…&lt;/a&gt; · &lt;a href="//en/10_when_notifications_explode_message_storms_pon.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML practical guide (PT)&lt;/td&gt;
&lt;td&gt;&lt;a href="//../artigos/27_guia_pratico_treino_ml_smart_brewery.md"&gt;&lt;code&gt;docs/artigos/27_guia_pratico_treino_ml_smart_brewery.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power BI / realtime notes&lt;/td&gt;
&lt;td&gt;&lt;a href="//../power-bi-realtime.md"&gt;&lt;code&gt;docs/power-bi-realtime.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>architecture</category>
      <category>computerscience</category>
      <category>resources</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Retrospective: lessons from building a reactive rules engine in Elixir</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:26:49 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/retrospective-lessons-from-building-a-reactive-rules-engine-in-elixir-1hcb</link>
      <guid>https://dev.to/matheuscamarques/retrospective-lessons-from-building-a-reactive-rules-engine-in-elixir-1hcb</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Retrospective: lessons from building a reactive rules engine in Elixir
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 12 of 12&lt;/strong&gt; — This post closes the arc that started with &lt;strong&gt;why&lt;/strong&gt; the BEAM fits reactive rules and ends with &lt;strong&gt;how&lt;/strong&gt; we measure them under load. &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;Part 11 on dev.to — Dev profiling: CPU, memory, and what changed after optimizations&lt;/a&gt; · &lt;a href="//11_dev_profiling_cpu_memory_optimizations.md"&gt;repo draft&lt;/a&gt; was about profilers and reproducible workloads; here I zoom out: &lt;strong&gt;integrations that paid off&lt;/strong&gt;, &lt;strong&gt;costs we accepted&lt;/strong&gt;, and &lt;strong&gt;what I would try next&lt;/strong&gt; if I were green-fielding tomorrow.&lt;/p&gt;

&lt;p&gt;This is not a substitute for the earlier technical posts—treat it as a &lt;strong&gt;map&lt;/strong&gt; and a &lt;strong&gt;checklist&lt;/strong&gt; for your own PON-style systems. Each part now ends with a &lt;strong&gt;References and further reading&lt;/strong&gt; section; the consolidated list lives in &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the twelve parts built
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stretch&lt;/th&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1–2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Notifications as the organizing principle; &lt;strong&gt;Facts&lt;/strong&gt;, &lt;strong&gt;Rules&lt;/strong&gt;, and &lt;strong&gt;Premises&lt;/strong&gt; as OTP processes and &lt;code&gt;Registry&lt;/code&gt; topics.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3–4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Less boilerplate via &lt;strong&gt;&lt;code&gt;defrule&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;defpremissa&lt;/code&gt;&lt;/strong&gt;; &lt;strong&gt;ports and adapters&lt;/strong&gt; so rules do not hard-code IO.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5–6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Smart Brewery&lt;/strong&gt; as a serious lab; &lt;strong&gt;LiveView&lt;/strong&gt; as the operator’s window with batching and streams.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;7–8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Broadway / GenStage&lt;/strong&gt;, &lt;strong&gt;TimescaleDB&lt;/strong&gt;, star-schema &lt;strong&gt;BI&lt;/strong&gt; and safe read-only roles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;ML export / import&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;ml_predictions&lt;/code&gt;&lt;/strong&gt; without blocking the engine.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10–11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;&lt;strong&gt;Part 10 on dev.to&lt;/strong&gt;&lt;/a&gt; — &lt;strong&gt;message storms&lt;/strong&gt;, dedup and fan-out coalescence; &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;&lt;strong&gt;Part 11 on dev.to&lt;/strong&gt;&lt;/a&gt; — &lt;strong&gt;CPU/memory profiling&lt;/strong&gt; with &lt;code&gt;PipelineWorkload&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together they describe one opinionated path: &lt;strong&gt;reactive core&lt;/strong&gt;, &lt;strong&gt;async edges&lt;/strong&gt;, &lt;strong&gt;honest measurement&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What worked well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Process boundaries match mental boundaries.&lt;/strong&gt; A &lt;code&gt;Fato&lt;/code&gt; is a named mailbox with a value; a &lt;code&gt;Regra&lt;/code&gt; subscribes and reacts. That maps cleanly to drawings on a whiteboard and to Elixir supervision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit message shapes.&lt;/strong&gt; Consumers understand &lt;code&gt;{:notificacao, name, value}&lt;/code&gt; and &lt;code&gt;{:notificacoes_lote, map}&lt;/code&gt;—whether they live in &lt;code&gt;tec0301_pon&lt;/code&gt; or in the Phoenix app. Ambiguity is expensive at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two applications, one story.&lt;/strong&gt; Keeping &lt;strong&gt;&lt;code&gt;tec0301_pon&lt;/code&gt;&lt;/strong&gt; as the engine and &lt;strong&gt;&lt;code&gt;simulacoes_visuais&lt;/code&gt;&lt;/strong&gt; as warehouse + UI avoided turning the rule engine into an Ecto or HTTP library, while still shipping a full twin demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batching at the pain points.&lt;/strong&gt; LiveView batchers, Broadway batchers, &lt;code&gt;Fanout.atualizar_lote/1&lt;/code&gt;, and async DB writers share the same philosophy: &lt;strong&gt;coalesce before fan-out or disk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telemetry as a spine, not an afterthought.&lt;/strong&gt; From rule firings to Broadway flushes, the codebase repeatedly uses &lt;code&gt;:telemetry.execute/3&lt;/code&gt; and named events so you can correlate “the twin moved” with “the UI updated” and “rows landed in TimescaleDB.” That does not replace profilers, but it &lt;strong&gt;anchors&lt;/strong&gt; them: when CPU spikes, you want a span or counter that says &lt;em&gt;which&lt;/em&gt; stage grew.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-offs we accepted
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DSL complexity.&lt;/strong&gt; Macros that generate rule modules are powerful and testable, but they are a onboarding cliff. Teams that fear metaprogramming will push for YAML or database rules—each has its own cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulation vs. fidelity vs. CPU.&lt;/strong&gt; Monte Carlo noise, strict &lt;code&gt;===&lt;/code&gt; deduplication, and float quantization are linked: quieter logs mean less work, but also less “physical” continuity unless you design for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pragmatic coupling in examples.&lt;/strong&gt; Rules that call &lt;code&gt;Adapters.PredioIO&lt;/code&gt; directly are easy to read; “production-shaped” indirection via &lt;code&gt;Application.get_env/3&lt;/code&gt; is more wiring. The series kept both stories visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational surface.&lt;/strong&gt; TimescaleDB, CAGGs, retention, Power BI roles, and ML export paths are &lt;strong&gt;optional&lt;/strong&gt; but real: the default &lt;code&gt;mix phx.server&lt;/code&gt; story is heavier than a pure library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;English drafts, Portuguese publication layer.&lt;/strong&gt; Keeping &lt;code&gt;docs/devto/en/*.md&lt;/code&gt; as the paste-ready source while &lt;code&gt;devto_serie_pon_smart_brewery.md&lt;/code&gt; tracks PT titles and dev.to slugs adds a bit of bookkeeping. The upside is a single technical narrative in one language and localized packaging when you hit publish.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would try earlier next time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profiling harness from week one&lt;/strong&gt; — &lt;code&gt;PipelineWorkload&lt;/code&gt;-style reproducible ticks save arguments about regressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One canonical env matrix&lt;/strong&gt; — document &lt;code&gt;SIMULACOES_TSDB_ENABLED&lt;/code&gt;, Monte Carlo interval, and Broadway batch sizes in a single table (the repo now spreads this across &lt;code&gt;performance-dev.md&lt;/code&gt; and config; still easy to drift).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable join keys&lt;/strong&gt; — align persisted &lt;code&gt;rule_events.regra_id&lt;/code&gt; with dimension tables (&lt;code&gt;r_01&lt;/code&gt; vs &lt;code&gt;"1"&lt;/code&gt;) in one migration or view, so BI and ML exports do not need ad hoc bridges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stricter “no work if unchanged” policy&lt;/strong&gt; — extend the dedup story to any hot path that allocates (e.g. telemetry encode) once profiling proves it matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are &lt;strong&gt;hypotheses&lt;/strong&gt;, not promises—verify on your workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code spine (the same system, twelve lenses)
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part 2 / 10 — fact update and dispatch only when the value changes:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/fato.ex (concept)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_cast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:atualizar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valor_igual?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c1"&gt;# … update state, ETS, Registry.dispatch → {:notificacao, nome, valor}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_estado&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Part 3 — DSL-shaped rule:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;defrule&lt;/span&gt; &lt;span class="no"&gt;RegraIrrigacao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:temp_ambiente&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:umidade_solo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:estado_bomba&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:nivel_tanque&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:temp_ambiente&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:umidade_solo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;BombaDeAgua&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ligar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:estado_bomba&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:ligada&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Part 4 — port = behaviour:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioAtuadores&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;ligar_luz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;trancar_porta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Part 6 — operator route:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="s2"&gt;"/smart-brewery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SmartBreweryLive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;
&lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="s2"&gt;"/smart-brewery/ml-predictions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;MlPredictionsLive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Part 7 — Broadway flush to PubSub and optional TSDB:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:simulacoes_visuais&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tsdb_enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryAsyncWriter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Part 9 — export and import (shell):&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix export.ml &lt;span class="nt"&gt;--out&lt;/span&gt; /tmp/ml_export &lt;span class="nt"&gt;--since-hours&lt;/span&gt; 168
mix import.ml.predictions &lt;span class="nt"&gt;--file&lt;/span&gt; /tmp/preds.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;[&lt;/em&gt;&lt;em&gt;Part 11 on dev.to&lt;/em&gt;&lt;em&gt;](&lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb&lt;/a&gt;) — reproducible load:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix profile.cprof &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SimulacoesVisuais.Profile.PipelineWorkload.run()"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  End-to-end picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph engine [tec0301_pon]
    F[Fato]
    R[Regra]
    A[Adapters]
  end
  subgraph app [simulacoes_visuais]
    MC[MonteCarlo]
    BW[Broadway]
    LV[LiveView]
    TSDB[(TimescaleDB)]
  end
  MC --&amp;gt; F
  F --&amp;gt; R
  R --&amp;gt; A
  F --&amp;gt; BW
  BW --&amp;gt; TSDB
  BW --&amp;gt; LV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Series index (parts 1–12 + consolidated bibliography)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Draft / published&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;Notification-Oriented Paradigm (PON) in Elixir: why the BEAM fits reactive rules&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/notification-oriented-paradigm-pon-in-elixir-why-the-beam-fits-reactive-rules-2p9e"&gt;dev.to&lt;/a&gt; · &lt;a href="//01_pon_in_elixir_why_beam.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;From whiteboard to code: mapping Facts, Rules, and Premises to OTP processes&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/from-whiteboard-to-code-mapping-facts-rules-and-premises-to-otp-processes-1blb"&gt;dev.to&lt;/a&gt; · &lt;a href="//02_from_whiteboard_to_code_otp.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A metaprogrammed DSL: &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; with less PON boilerplate&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;dev.to&lt;/a&gt; · &lt;a href="//03_metaprogrammed_dsl_defrule_defpremissa.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Hexagonal architecture + PON: Ports &amp;amp; Adapters to decouple the engine&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;dev.to&lt;/a&gt; · &lt;a href="//04_hexagonal_pon_ports_adapters.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Smart Brewery: a digital twin brewery as a PON lab&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;dev.to&lt;/a&gt; · &lt;a href="//05_smart_brewery_digital_twin_pon_lab.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Phoenix LiveView in real time: an operations UI on top of a rules engine&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;dev.to&lt;/a&gt; · &lt;a href="//06_phoenix_liveview_operations_ui_rules_engine.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;dev.to&lt;/a&gt; · &lt;a href="//07_from_simulation_to_storage_telemetry_broadway_timescaledb.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;dev.to&lt;/a&gt; · &lt;a href="//08_bi_without_mystery_dimensions_facts_power_bi.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;ML on the digital twin: export, train pilots, and import predictions back into the app&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;dev.to&lt;/a&gt; · &lt;a href="//09_ml_digital_twin_export_train_import_predictions.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;When notifications explode: message storms, deduplication, and back-pressure in PON&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;dev.to&lt;/a&gt; · &lt;a href="//10_when_notifications_explode_message_storms_pon.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Dev profiling: CPU, memory, and what changed after optimizations&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;dev.to&lt;/a&gt; · &lt;a href="//11_dev_profiling_cpu_memory_optimizations.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Retrospective: lessons from building a reactive rules engine in Elixir&lt;/td&gt;
&lt;td&gt;&lt;em&gt;this post&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Bibliography — PON + Smart Brewery dev.to series (EN drafts)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;dev.to&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Portuguese titles and publication notes for dev.to live in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading (series-level)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Master bibliography&lt;/strong&gt; — normalized table of books, papers, and docs used across Parts 1–12: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NOP / PON&lt;/strong&gt; — Simão et al. (2013) comparative study — &lt;a href="https://www.scirp.org/journal/paperinformation?paperid=19842" rel="noopener noreferrer"&gt;SCIRP&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTP / BEAM&lt;/strong&gt; — Armstrong (2003) thesis — &lt;a href="https://www.erlang.org/download/armstrong_thesis_2003.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;; Cesarini &amp;amp; Thompson, &lt;em&gt;Programming Erlang&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hexagonal&lt;/strong&gt; — Cockburn — &lt;a href="https://alistair.cockburn.us/hexagonal-architecture/" rel="noopener noreferrer"&gt;ports and adapters&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data + time-series&lt;/strong&gt; — Kimball &amp;amp; Ross (dimensional modeling); TimescaleDB — &lt;a href="https://docs.timescale.com/" rel="noopener noreferrer"&gt;docs.timescale.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML readiness&lt;/strong&gt; — Breck et al., &lt;em&gt;ML Test Score&lt;/em&gt; — &lt;a href="https://research.google/pubs/pub46555/" rel="noopener noreferrer"&gt;Google Research&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Thank you
&lt;/h2&gt;

&lt;p&gt;If you followed from &lt;a href="https://dev.to/matheuscamarques/notification-oriented-paradigm-pon-in-elixir-why-the-beam-fits-reactive-rules-2p9e"&gt;Part 1 on dev.to&lt;/a&gt;: you have seen &lt;strong&gt;one&lt;/strong&gt; way to marry reactive rules, OTP, and industrial-style twins—not the only way. Take the &lt;strong&gt;patterns&lt;/strong&gt; (boundaries, batching, measurement) and adapt the &lt;strong&gt;machinery&lt;/strong&gt; to your domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End of series.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;Part 11 on dev.to — Dev profiling: CPU, memory, and what changed after optimizations&lt;/a&gt; · &lt;a href="//11_dev_profiling_cpu_memory_optimizations.md"&gt;repo draft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code map (monorepo root):&lt;/strong&gt; &lt;code&gt;lib/tec0301_pon/&lt;/code&gt;, &lt;code&gt;apps/simulacoes_visuais/&lt;/code&gt;, &lt;code&gt;docs/performance-dev.md&lt;/code&gt;, &lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>architecture</category>
      <category>timescaledb</category>
      <category>liveview</category>
    </item>
    <item>
      <title>Dev profiling: CPU, memory, and what changed after optimizations</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:22:45 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb</link>
      <guid>https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Dev profiling: CPU, memory, and what changed after optimizations
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 11 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;Part 10 on dev.to — When notifications explode: message storms, deduplication, and back-pressure in PON&lt;/a&gt; · &lt;a href="//10_when_notifications_explode_message_storms_pon.md"&gt;repo draft&lt;/a&gt; described &lt;strong&gt;deduplication&lt;/strong&gt;, &lt;strong&gt;fan-out batching&lt;/strong&gt;, and &lt;strong&gt;mailbox draining&lt;/strong&gt; in the PON core. Those changes are meant to reduce wasted work—but you only know they helped if you &lt;strong&gt;measure&lt;/strong&gt; the same workload twice under the same knobs.&lt;/p&gt;

&lt;p&gt;This post is a practical guide to &lt;strong&gt;dev profiling&lt;/strong&gt; in this monorepo: reproducible load via &lt;strong&gt;&lt;code&gt;SimulacoesVisuais.Profile.PipelineWorkload&lt;/code&gt;&lt;/strong&gt;, Mix tasks built on OTP profilers, and how to read results without fooling yourself. Deep dives and longer checklists live in &lt;strong&gt;&lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;docs/performance-dev.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="//../../memory-pressure-heuristics.md"&gt;&lt;code&gt;docs/memory-pressure-heuristics.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. &lt;strong&gt;Part 12&lt;/strong&gt; closes the series with a retrospective.&lt;/p&gt;




&lt;h2&gt;
  
  
  What profilers actually show
&lt;/h2&gt;

&lt;p&gt;On the BEAM, &lt;strong&gt;&lt;code&gt;mix profile.cprof&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;eprof&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;fprof&lt;/code&gt;&lt;/strong&gt;, and (OTP 27+) &lt;strong&gt;&lt;code&gt;tprof&lt;/code&gt;&lt;/strong&gt; answer different questions (official task docs under &lt;a href="https://hexdocs.pm/mix/Mix.Tasks.Profile.Cprof.html" rel="noopener noreferrer"&gt;Mix.Tasks.Profile&lt;/a&gt; on HexDocs; flags and caveats in this repo are expanded in &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Rough question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cprof&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which functions ran &lt;strong&gt;most often&lt;/strong&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;eprof&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Where did &lt;strong&gt;this process&lt;/strong&gt; spend time?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;fprof&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What does the &lt;strong&gt;call graph&lt;/strong&gt; look like in time?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;tprof&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Aggregated &lt;strong&gt;time&lt;/strong&gt;, &lt;strong&gt;calls&lt;/strong&gt;, or &lt;strong&gt;allocation&lt;/strong&gt; (WORDS) across processes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of them is a direct “heap map.” &lt;strong&gt;RAM pressure&lt;/strong&gt; still needs &lt;code&gt;:erlang.memory/0&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;Process.info/2&lt;/code&gt;&lt;/strong&gt;, Observer, or LiveDashboard VM metrics—hot code paths that allocate a lot often correlate with high call counts, but correlation is not identity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducible load: &lt;code&gt;PipelineWorkload&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The module &lt;strong&gt;&lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/profile/pipeline_workload.ex"&gt;&lt;code&gt;SimulacoesVisuais.Profile.PipelineWorkload&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; runs &lt;strong&gt;Monte Carlo ticks&lt;/strong&gt; through the same stack as production-style simulation: PON facts, hybrid models, telemetry fan-out, and (if enabled) TSDB writers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.Profile.PipelineWorkload.run/1 (opening)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;duration_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:duration_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;env_duration_ms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;ticks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:ticks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PROFILE_PIPELINE_TICKS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;max_ticks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:max_ticks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PROFILE_PIPELINE_MAX_TICKS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5_000_000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode_from_env&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="n"&gt;memory?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env_bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"PROFILE_PIPELINE_MEMORY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_all_started&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:simulacoes_visuais&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;MonteCarlo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_loop&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;memory?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;print_memory_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"before"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# … duration_ms branch → run_until_deadline_* OR fixed tick count …&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;memory?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;print_memory_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"after"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="ss"&gt;:ok&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Two modes matter for interpretation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;:via_genserver&lt;/code&gt;&lt;/strong&gt; (default) — calls &lt;strong&gt;&lt;code&gt;SmartBreweryMonteCarlo.run_tick_sync/0&lt;/code&gt;&lt;/strong&gt;. Realistic for end-to-end behavior; some profilers attribute time to the &lt;strong&gt;caller&lt;/strong&gt; (e.g. Mix) as much as to the GenServer body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;:in_process&lt;/code&gt;&lt;/strong&gt; — &lt;strong&gt;&lt;code&gt;PROFILE_PIPELINE_MODE=in_process&lt;/code&gt;&lt;/strong&gt; runs &lt;strong&gt;&lt;code&gt;run_tick_pure/1&lt;/code&gt;&lt;/strong&gt; in the &lt;strong&gt;current&lt;/strong&gt; process. Better for &lt;strong&gt;&lt;code&gt;fprof&lt;/code&gt;&lt;/strong&gt;-style call graphs; &lt;strong&gt;does not&lt;/strong&gt; advance the live GenServer RNG state—profiling only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set duration instead of a fixed tick count with &lt;strong&gt;&lt;code&gt;PROFILE_PIPELINE_DURATION_MS&lt;/code&gt;&lt;/strong&gt; (milliseconds wall clock), capped by &lt;strong&gt;&lt;code&gt;PROFILE_PIPELINE_MAX_TICKS&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment variables (cheat sheet)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROFILE_PIPELINE_TICKS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixed number of ticks when duration is off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROFILE_PIPELINE_DURATION_MS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run until wall-clock deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROFILE_PIPELINE_MAX_TICKS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Safety ceiling for duration mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROFILE_PIPELINE_MODE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;via_genserver&lt;/code&gt; (default) or &lt;code&gt;in_process&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROFILE_PIPELINE_MEMORY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt; / &lt;code&gt;true&lt;/code&gt; → print &lt;code&gt;:erlang.memory/0&lt;/code&gt; + key processes before/after&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SIMULACOES_TSDB_ENABLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Match your DB-on vs DB-off scenario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOGGER_LEVEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Often &lt;code&gt;warning&lt;/code&gt; to reduce log noise during long runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For &lt;strong&gt;before/after&lt;/strong&gt; comparisons, also lock Monte Carlo and pipeline tuning: &lt;code&gt;MONTE_CARLO_INTERVAL_MS&lt;/code&gt;, &lt;code&gt;MONTE_CARLO_FACTS_PER_TICK_MIN&lt;/code&gt; / &lt;code&gt;MAX&lt;/code&gt;, &lt;code&gt;TELEMETRY_PIPELINE_BATCH_SIZE&lt;/code&gt;, &lt;code&gt;TELEMETRY_PIPELINE_BATCH_TIMEOUT_MS&lt;/code&gt;, &lt;code&gt;TELEMETRY_PRODUCER_MAX_QUEUE&lt;/code&gt;, &lt;code&gt;OEE_PUBSUB_MIN_INTERVAL_MS&lt;/code&gt;—see the tables in &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick commands
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Call-count hotspot (cprof), TSDB off, long window:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/simulacoes_visuais
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tmp/profile
&lt;span class="nv"&gt;PROFILE_PIPELINE_DURATION_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120000 &lt;span class="nv"&gt;LOGGER_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;warning &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SIMULACOES_TSDB_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  mix profile.cprof &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SimulacoesVisuais.Profile.PipelineWorkload.run()"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee &lt;/span&gt;tmp/profile/cprof-sample.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Narrow to one module:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix profile.cprof &lt;span class="nt"&gt;--module&lt;/span&gt; SimulacoesVisuais.SmartBreweryMonteCarlo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SimulacoesVisuais.Profile.PipelineWorkload.run()"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Memory snapshot without a profiler:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROFILE_PIPELINE_MEMORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;PROFILE_PIPELINE_TICKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30 &lt;span class="se"&gt;\&lt;/span&gt;
  mix profile.pipeline
&lt;span class="c"&gt;# alias for: mix simulacoes_visuais.profile_workload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;OTP 27+ aggregated time:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROFILE_PIPELINE_DURATION_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120000 &lt;span class="nv"&gt;LOGGER_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;warning &lt;span class="se"&gt;\&lt;/span&gt;
  mix profile.tprof &lt;span class="nt"&gt;--type&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="nt"&gt;--report&lt;/span&gt; total &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SimulacoesVisuais.Profile.PipelineWorkload.run()"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;fprof trace file&lt;/strong&gt; — put &lt;strong&gt;&lt;code&gt;-e "..."&lt;/code&gt; before &lt;code&gt;--trace-to-file&lt;/code&gt;&lt;/strong&gt;, or Mix mis-parses the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix profile.fprof &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SimulacoesVisuais.Profile.PipelineWorkload.run()"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trace-to-file&lt;/span&gt; tmp/profile/run.trace &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee &lt;/span&gt;tmp/profile/fprof-out.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  60-second battery
&lt;/h2&gt;

&lt;p&gt;From the repo root, &lt;strong&gt;&lt;a href="//../../scripts/run_profile_60s.sh"&gt;&lt;code&gt;scripts/run_profile_60s.sh&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; runs several profiler variants and writes under &lt;strong&gt;&lt;code&gt;apps/simulacoes_visuais/tmp/profile/*-60s.txt&lt;/code&gt;&lt;/strong&gt;. Useful when you want comparable artifacts after a change without hand-typing flags.&lt;/p&gt;




&lt;h2&gt;
  
  
  Profiling the Monte Carlo GenServer itself
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;&lt;code&gt;PipelineWorkload&lt;/code&gt;&lt;/strong&gt; in default &lt;strong&gt;&lt;code&gt;via_genserver&lt;/code&gt;&lt;/strong&gt; mode, &lt;strong&gt;&lt;code&gt;eprof&lt;/code&gt;&lt;/strong&gt; attached to the Mix process often shows &lt;strong&gt;&lt;code&gt;GenServer.call&lt;/code&gt;&lt;/strong&gt; / client time—not the full body of &lt;strong&gt;&lt;code&gt;SmartBreweryMonteCarlo&lt;/code&gt;&lt;/strong&gt;. To attribute work to the right OTP process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the app (&lt;code&gt;mix phx.server&lt;/code&gt; or &lt;code&gt;Application.ensure_all_started(:simulacoes_visuais)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Resolve PIDs: &lt;code&gt;Process.whereis(SimulacoesVisuais.SmartBreweryMonteCarlo)&lt;/code&gt;, and when TSDB is on, writers such as &lt;strong&gt;&lt;code&gt;RuleEventWriter&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Attach &lt;strong&gt;&lt;code&gt;eprof&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;fprof&lt;/code&gt;&lt;/strong&gt; to those PIDs from an &lt;strong&gt;&lt;code&gt;iex&lt;/code&gt;&lt;/strong&gt; session on the same node (see OTP profiling docs), while generating load from &lt;strong&gt;another&lt;/strong&gt; shell or the UI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cross-check &lt;strong&gt;reductions&lt;/strong&gt; and &lt;strong&gt;mailbox length&lt;/strong&gt; in Observer or LiveDashboard while &lt;strong&gt;&lt;code&gt;PROFILE_PIPELINE_MEMORY=1&lt;/code&gt;&lt;/strong&gt; prints heap and queue depth for the registered names the workload already tracks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Baselines: “light” vs “heavy” server
&lt;/h2&gt;

&lt;p&gt;Before blaming PON, confirm whether you are in a &lt;strong&gt;quiet&lt;/strong&gt; or &lt;strong&gt;noisy&lt;/strong&gt; dev profile. &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt; suggests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lighter: no TSDB, no auto Monte Carlo&lt;/span&gt;
&lt;span class="nv"&gt;PHX_LV_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;SIMULACOES_TSDB_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false &lt;/span&gt;&lt;span class="nv"&gt;AUTO_START_MONTE_CARLO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false &lt;/span&gt;mix phx.server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Heavier: TSDB, fast MC, more LiveView debug&lt;/span&gt;
&lt;span class="nv"&gt;PHX_LV_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;SIMULACOES_TSDB_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="nv"&gt;AUTO_START_MONTE_CARLO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;MONTE_CARLO_INTERVAL_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 mix phx.server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profiling workloads should use the &lt;strong&gt;same&lt;/strong&gt; economic assumptions as the hypothesis you are testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimizations this repo ties to measurements
&lt;/h2&gt;

&lt;p&gt;Internal write-ups (Portuguese) connect architecture to numbers: &lt;strong&gt;&lt;a href="//../../docs/artigos/18_arquitetura_runtime_smart_brewery_e_resultados_de_desempenho.md"&gt;article 18&lt;/a&gt;&lt;/strong&gt; (runtime), &lt;strong&gt;&lt;a href="//../../docs/artigos/19_mitigacao_message_storm_pon_elixir_smart_brewery.md"&gt;article 19&lt;/a&gt;&lt;/strong&gt; (message storm mitigation), &lt;strong&gt;&lt;a href="//../../docs/artigos/20_otimizacao_simulacao_hibrida_pon_elixir.md"&gt;article 20&lt;/a&gt;&lt;/strong&gt; (ETS &lt;code&gt;write_concurrency&lt;/code&gt;, Registry partitions, Rete spike, Broadway tuning). The &lt;strong&gt;checklist&lt;/strong&gt; at the top of &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt; maps each item to modules—for example: &lt;strong&gt;&lt;code&gt;Registry&lt;/code&gt; partitions&lt;/strong&gt; in &lt;code&gt;Tec0301Pon.Application&lt;/code&gt;, &lt;strong&gt;ETS&lt;/strong&gt; &lt;code&gt;read_concurrency&lt;/code&gt; / &lt;code&gt;write_concurrency&lt;/code&gt; on fact values, &lt;strong&gt;Broadway&lt;/strong&gt; &lt;code&gt;:telemetry_pipeline_processor_concurrency&lt;/code&gt; / batcher settings in config, &lt;strong&gt;&lt;code&gt;Fanout&lt;/code&gt;&lt;/strong&gt; switching to &lt;strong&gt;&lt;code&gt;Task.async_stream&lt;/code&gt;&lt;/strong&gt; when the update map has more than four pairs, and optional &lt;strong&gt;Rete&lt;/strong&gt; experiments via &lt;code&gt;Tec0301Pon.PON.ReteSpike&lt;/code&gt;. Profiling tells you which row in that table actually matters for &lt;em&gt;your&lt;/em&gt; tick rate and hardware.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; publish a single “we saved 40%” headline unless you have &lt;strong&gt;paired&lt;/strong&gt; runs (same commit range, same env, same &lt;code&gt;PROFILE_PIPELINE_*&lt;/code&gt;) and you document where the files live—otherwise you are storytelling, not benchmarking.&lt;/p&gt;




&lt;h2&gt;
  
  
  BI / TSDB sanity (one paragraph)
&lt;/h2&gt;

&lt;p&gt;If charts look empty while &lt;code&gt;telemetry_events&lt;/code&gt; has millions of rows, check &lt;strong&gt;time windows&lt;/strong&gt;: &lt;code&gt;mix verify.bi&lt;/code&gt; uses small &lt;strong&gt;&lt;code&gt;LIMIT&lt;/code&gt;&lt;/strong&gt; samples and often &lt;strong&gt;last 24h&lt;/strong&gt; in SQL; &lt;strong&gt;&lt;code&gt;mix verify.tsdb&lt;/code&gt;&lt;/strong&gt; reports &lt;strong&gt;global&lt;/strong&gt; counts and &lt;strong&gt;&lt;code&gt;MAX(ts)&lt;/code&gt;&lt;/strong&gt;. Misaligned windows are a data issue, not a broken query—see &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt; § BI native.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flow under profiling
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  PW[PipelineWorkload]
  MC[SmartBreweryMonteCarlo]
  PON[PON Fato Regra]
  BW[Broadway Telemetry]
  W[AsyncWriters optional]
  Mix[Mix process profiler]
  PW --&amp;gt; MC
  MC --&amp;gt; PON
  PON --&amp;gt; BW
  BW --&amp;gt; W
  Mix --&amp;gt;|"via_genserver: often attributes client time"| MC
  Mix --&amp;gt;|"in_process: run_tick_pure only"| MC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 10&lt;/strong&gt; reduced redundant messages; &lt;strong&gt;Part 11&lt;/strong&gt; gives you the &lt;strong&gt;bench&lt;/strong&gt;: &lt;code&gt;PipelineWorkload&lt;/code&gt;, Mix profilers, memory snapshots, and explicit env discipline. Treat every optimization as a &lt;strong&gt;hypothesis&lt;/strong&gt; until two traces agree. &lt;strong&gt;Part 12&lt;/strong&gt; zooms out with a series retrospective and a full index of posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mix profiling&lt;/strong&gt; — &lt;code&gt;profile.cprof&lt;/code&gt;, &lt;code&gt;profile.eprof&lt;/code&gt;, &lt;code&gt;profile.fprof&lt;/code&gt;, &lt;code&gt;profile.tprof&lt;/code&gt; — &lt;a href="https://hexdocs.pm/mix/Mix.Tasks.Profile.Cprof.html" rel="noopener noreferrer"&gt;HexDocs (cprof)&lt;/a&gt; and sibling tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Armstrong (2003)&lt;/strong&gt; — process isolation and failure domains — &lt;a href="https://www.erlang.org/download/armstrong_thesis_2003.pdf" rel="noopener noreferrer"&gt;thesis PDF&lt;/a&gt; (context for why mailbox/profiler stories matter).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/profile/pipeline_workload.ex"&gt;&lt;code&gt;pipeline_workload.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../docs/performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../docs/memory-pressure-heuristics.md"&gt;&lt;code&gt;memory-pressure-heuristics.md&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../scripts/run_profile_60s.sh"&gt;&lt;code&gt;run_profile_60s.sh&lt;/code&gt;&lt;/a&gt;; internal articles &lt;a href="//../../docs/artigos/18_arquitetura_runtime_smart_brewery_e_resultados_de_desempenho.md"&gt;18&lt;/a&gt;, &lt;a href="//../../docs/artigos/20_otimizacao_simulacao_hibrida_pon_elixir.md"&gt;20&lt;/a&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;Dev profiling: CPU, memory, and what changed after optimizations&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;Part 10 on dev.to — When notifications explode: message storms, deduplication, and back-pressure in PON&lt;/a&gt; · &lt;a href="//10_when_notifications_explode_message_storms_pon.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="//12_retrospective_reactive_rules_engine_elixir.md"&gt;Part 12 — Retrospective: lessons from building a reactive rules engine in Elixir&lt;/a&gt; — &lt;em&gt;end of series&lt;/em&gt; (index + lessons learned).&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>performance</category>
      <category>architecture</category>
      <category>profiling</category>
    </item>
    <item>
      <title>When notifications explode: message storms, deduplication, and back-pressure in PON</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:18:07 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4</link>
      <guid>https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  When notifications explode: message storms, deduplication, and back-pressure in PON
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 10 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;Part 9 on dev.to — ML on the digital twin: export, train pilots, and import predictions back into the app&lt;/a&gt; · &lt;a href="//09_ml_digital_twin_export_train_import_predictions.md"&gt;repo draft&lt;/a&gt; showed how to &lt;strong&gt;export&lt;/strong&gt; history and &lt;strong&gt;import&lt;/strong&gt; model scores. Before adding more consumers, it pays to ask: what happens when the twin’s &lt;strong&gt;tick rate&lt;/strong&gt; rises and every &lt;code&gt;Fato.atualizar/2&lt;/code&gt; fans out to many &lt;code&gt;Regra&lt;/code&gt; processes?&lt;/p&gt;

&lt;p&gt;This post is about &lt;strong&gt;message storms&lt;/strong&gt; on the BEAM: redundant &lt;code&gt;Registry.dispatch&lt;/code&gt; work, rule mailboxes full of duplicate context, and &lt;strong&gt;back-pressure&lt;/strong&gt; at the edges. The patterns below are implemented in &lt;strong&gt;&lt;code&gt;tec0301_pon&lt;/code&gt;&lt;/strong&gt; and exercised by &lt;strong&gt;Smart Brewery&lt;/strong&gt; + &lt;strong&gt;&lt;code&gt;simulacoes_visuais&lt;/code&gt;&lt;/strong&gt;. Deeper Portuguese notes live in &lt;a href="//../../artigos/19_mitigacao_message_storm_pon_elixir_smart_brewery.md"&gt;&lt;code&gt;docs/artigos/19_mitigacao_message_storm_pon_elixir_smart_brewery.md&lt;/code&gt;&lt;/a&gt;; reproducing profiles is covered in &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;docs/performance-dev.md&lt;/code&gt;&lt;/a&gt;. &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;&lt;strong&gt;Part 11 on dev.to&lt;/strong&gt;&lt;/a&gt; zooms in on &lt;strong&gt;profiling&lt;/strong&gt; methodology and before/after numbers. Intuitively, &lt;strong&gt;stable&lt;/strong&gt; queueing systems obey &lt;strong&gt;Little’s Law&lt;/strong&gt; (mean work in system ∝ arrival rate × mean delay—&lt;a href="https://en.wikipedia.org/wiki/Little%27s_law" rel="noopener noreferrer"&gt;Little 1961&lt;/a&gt;); storms push the “work in system” term through the roof unless you cut redundant arrivals or widen the bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Symptom: CPU spent orchestrating, not deciding
&lt;/h2&gt;

&lt;p&gt;In actor-style PON graphs, one fact update can mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;GenServer&lt;/code&gt;&lt;/strong&gt; cast on the fact process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Registry.dispatch&lt;/code&gt;&lt;/strong&gt; to every subscriber.&lt;/li&gt;
&lt;li&gt;One &lt;strong&gt;message per rule&lt;/strong&gt; that watches the fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition evaluation&lt;/strong&gt; and possibly actions—again per message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Under Monte Carlo or dense PLC simulation, steps 2–3 multiply into a &lt;strong&gt;storm&lt;/strong&gt;: schedulers stay busy copying terms between processes, even when the &lt;strong&gt;business outcome&lt;/strong&gt; would be the same after coalescing updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Deduplicate at the source (&lt;code&gt;Fato&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;If the new value is &lt;strong&gt;strictly equal&lt;/strong&gt; to the current one (&lt;code&gt;===&lt;/code&gt;), skip dispatch entirely and do not bump the fact’s notification statistics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/fato.ex — handle_cast/2 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_cast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:atualizar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valor_igual?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;novo_estado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;estado&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:estatisticas&lt;/span&gt;&lt;span class="p"&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;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&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;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:valor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ets_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;inscritos&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;inscritos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:notificacao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_estado&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Float note:&lt;/strong&gt; equality is exact. Noisy floats should be &lt;strong&gt;quantized&lt;/strong&gt; upstream (as in the Smart Brewery random walk) if you rely on this filter—adding a global epsilon for all types would surprise rules that compare structured terms.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Coalesce across facts (&lt;code&gt;Fanout&lt;/code&gt; + &lt;code&gt;atualizar_lote/1&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Correlated equipment often updates &lt;strong&gt;several&lt;/strong&gt; facts in the same tick (e.g. filter pressure, clarity, pump speed). Calling &lt;code&gt;atualizar/2&lt;/code&gt; three times means three dispatches and three bursts to shared rules. &lt;strong&gt;&lt;code&gt;Fato.atualizar_lote/1&lt;/code&gt;&lt;/strong&gt; applies &lt;strong&gt;&lt;code&gt;{:atualizar_sem_dispatch, val}&lt;/code&gt;&lt;/strong&gt; per changed fact, then sends &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;{:notificacoes_lote, map}&lt;/code&gt; per subscriber PID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/fanout.ex — notify_lote/1 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;notify_lote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;changed&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flat_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@pubsub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniq&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:notificacoes_lote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules merge only the keys they watch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/regra.ex — after {:notificacoes_lote, updates}&lt;/span&gt;
&lt;span class="n"&gt;relevante&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;nova_memoria&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevante&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nova_memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drained&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;drain_notificacoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nova_memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&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="c1"&gt;# then update :estatisticas_notificacoes and call avaliar_apos_memoria/2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Large batches use &lt;strong&gt;&lt;code&gt;Task.async_stream&lt;/code&gt;&lt;/strong&gt; with bounded concurrency inside &lt;code&gt;Fanout.atualizar_lote/1&lt;/code&gt;; very small maps update sequentially—see the module for thresholds.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Drain the rule mailbox before evaluating (&lt;code&gt;drain_notificacoes/3&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Even with fan-out reduced, bursts still happen. After applying the first notification, &lt;strong&gt;&lt;code&gt;Regra&lt;/code&gt;&lt;/strong&gt; empties the mailbox of &lt;strong&gt;more&lt;/strong&gt; &lt;code&gt;{:notificacao, …}&lt;/code&gt; / &lt;code&gt;{:notificacoes_lote, …}&lt;/code&gt; with a zero-timeout &lt;code&gt;receive&lt;/code&gt;, then runs &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;avaliar_condicao/2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/regra.ex — drain (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;drain_notificacoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;receive&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:notificacao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;drain_notificacoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&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="ss"&gt;:notificacoes_lote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;drain_notificacoes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&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="k"&gt;after&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;debounce-by-draining&lt;/strong&gt;: the rule sees the &lt;strong&gt;latest&lt;/strong&gt; memory state for the burst window, not every intermediate message.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Fast reads without blocking the fact (&lt;code&gt;ETS&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Hot paths that only need the current value (e.g. filling rule memory, Monte Carlo readers) call &lt;strong&gt;&lt;code&gt;Fato.obter/1&lt;/code&gt;&lt;/strong&gt;, which prefers a public &lt;strong&gt;ETS&lt;/strong&gt; table with &lt;code&gt;read_concurrency&lt;/code&gt; and &lt;code&gt;write_concurrency&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/fato.ex — obter/1 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;obter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_atom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="ss"&gt;:ets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;@ets_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:obter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Tec0301Pon.Application&lt;/code&gt; calls &lt;strong&gt;&lt;code&gt;Fato.ensure_ets!()&lt;/code&gt;&lt;/strong&gt; before registering the &lt;code&gt;Registry&lt;/code&gt; so lookups are safe during startup races.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Back-pressure outside the engine (Phoenix / Broadway)
&lt;/h2&gt;

&lt;p&gt;The PON core is not the only fan-out. &lt;strong&gt;&lt;code&gt;SmartBreweryFactBroadcaster&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;LiveViewEventBatcher&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;TelemetryProducer&lt;/code&gt;&lt;/strong&gt; (bounded queue with drop-oldest), and &lt;strong&gt;Broadway&lt;/strong&gt; batchers implement &lt;strong&gt;pressure&lt;/strong&gt; so PostgreSQL and LiveView do not amplify storms—see &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to&lt;/a&gt; · &lt;a href="//06_phoenix_liveview_operations_ui_rules_engine.md"&gt;repo draft&lt;/a&gt; and &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;Part 7 on dev.to&lt;/a&gt; · &lt;a href="//07_from_simulation_to_storage_telemetry_broadway_timescaledb.md"&gt;repo draft&lt;/a&gt;. &lt;strong&gt;&lt;code&gt;SmartBreweryMonteCarlo&lt;/code&gt;&lt;/strong&gt; caches &lt;code&gt;Application.get_env&lt;/code&gt; bounds in &lt;strong&gt;&lt;code&gt;init&lt;/code&gt;&lt;/strong&gt; so the tick loop does not hit the application environment on every iteration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Message contract cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;Typical producer&lt;/th&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{:notificacao, name, value}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Fato&lt;/code&gt; after a real change&lt;/td&gt;
&lt;td&gt;Rules, telemetry bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{:notificacoes_lote, %{atom =&amp;gt; value}}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Fanout.notify_lote/1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same subscribers; rules &lt;code&gt;Map.take/2&lt;/code&gt; relevant keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Code that only ever calls &lt;strong&gt;&lt;code&gt;atualizar/2&lt;/code&gt;&lt;/strong&gt; keeps the single-notification shape; simulation paths that batch use &lt;strong&gt;&lt;code&gt;atualizar_lote/1&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measuring (preview — full walkthrough in &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;Part 11 on dev.to&lt;/a&gt;)
&lt;/h2&gt;

&lt;p&gt;The repo ships &lt;strong&gt;&lt;code&gt;SimulacoesVisuais.Profile.PipelineWorkload&lt;/code&gt;&lt;/strong&gt; and Mix tasks such as &lt;strong&gt;&lt;code&gt;mix profile.cprof&lt;/code&gt;&lt;/strong&gt; for repeatable runs. Example harness (TSDB off, headless):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apps/simulacoes_visuais
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tmp/profile
&lt;span class="nv"&gt;PROFILE_PIPELINE_DURATION_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;60000 &lt;span class="nv"&gt;LOGGER_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;warning &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SIMULACOES_TSDB_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false &lt;/span&gt;&lt;span class="nv"&gt;SIMULACOES_HEADLESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  mix profile.cprof &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SimulacoesVisuais.Profile.PipelineWorkload.run()"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee &lt;/span&gt;tmp/profile/cprof-sample.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat &lt;strong&gt;percent gains&lt;/strong&gt; as &lt;strong&gt;hypotheses&lt;/strong&gt; until you A/B the same workload on two commits; the internal article avoids advertising fixed “30–50%” without paired measurements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flow diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph reduce [Reduce fan-out]
    F[Fato atualizar]
    D{Value changed?}
    FD[Registry dispatch]
    L[Fato atualizar_lote]
    FN[Fanout notify_lote]
  end
  subgraph rule [Rule process]
    M[Mailbox]
    DR[drain_notificacoes]
    EV[avaliar_condicao once]
  end
  F --&amp;gt; D
  D --&amp;gt;|no| X[no notify]
  D --&amp;gt;|yes| FD
  FD --&amp;gt; M
  L --&amp;gt; FN
  FN --&amp;gt; M
  M --&amp;gt; DR --&amp;gt; EV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Message storms&lt;/strong&gt; are a property of &lt;strong&gt;graph fan-out&lt;/strong&gt;, not a judgment on PON. This codebase mitigates them with &lt;strong&gt;deduplication&lt;/strong&gt;, &lt;strong&gt;batch notifications&lt;/strong&gt;, &lt;strong&gt;mailbox draining&lt;/strong&gt;, &lt;strong&gt;ETS reads&lt;/strong&gt;, and &lt;strong&gt;bounded&lt;/strong&gt; telemetry pipelines—without changing what a rule &lt;em&gt;means&lt;/em&gt;, only how often it is prodded. &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;&lt;strong&gt;Part 11 on dev.to&lt;/strong&gt;&lt;/a&gt; ties these knobs to &lt;strong&gt;profiles&lt;/strong&gt; and memory heuristics.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Little, J. D. C. (1961)&lt;/strong&gt; — &lt;em&gt;A proof for the queuing formula L = λW&lt;/em&gt; — classic &lt;strong&gt;Little’s Law&lt;/strong&gt;; &lt;a href="https://en.wikipedia.org/wiki/Little%27s_law" rel="noopener noreferrer"&gt;Wikipedia entry&lt;/a&gt; with citation to &lt;em&gt;Operations Research&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Erlang&lt;/strong&gt; — &lt;code&gt;:erlang.process_info/2&lt;/code&gt; (mailbox length, etc.) — &lt;a href="https://www.erlang.org/doc/man/erlang.html" rel="noopener noreferrer"&gt;manual&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elixir &lt;code&gt;Registry&lt;/code&gt;&lt;/strong&gt; — dispatch semantics — &lt;a href="https://hexdocs.pm/elixir/Registry.html" rel="noopener noreferrer"&gt;HexDocs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="//../../lib/tec0301_pon/pon/fato.ex"&gt;&lt;code&gt;fato.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../lib/tec0301_pon/pon/fanout.ex"&gt;&lt;code&gt;fanout.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../lib/tec0301_pon/pon/regra.ex"&gt;&lt;code&gt;regra.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../lib/tec0301_pon/application.ex"&gt;&lt;code&gt;application.ex&lt;/code&gt;&lt;/a&gt;; &lt;a href="//../../docs/artigos/19_mitigacao_message_storm_pon_elixir_smart_brewery.md"&gt;&lt;code&gt;artigo 19&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../docs/performance-dev.md"&gt;&lt;code&gt;performance-dev.md&lt;/code&gt;&lt;/a&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;When notifications explode: message storms, deduplication, and back-pressure in PON&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;Part 9 on dev.to — ML on the digital twin: export, train pilots, and import predictions back into the app&lt;/a&gt; · &lt;a href="//09_ml_digital_twin_export_train_import_predictions.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;Part 11 on dev.to — Dev profiling: CPU, memory, and what changed after optimizations&lt;/a&gt; · &lt;a href="//11_dev_profiling_cpu_memory_optimizations.md"&gt;repo draft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>architecture</category>
      <category>performance</category>
      <category>realtime</category>
    </item>
    <item>
      <title>ML on the digital twin: export, train pilots, and import predictions back into the app</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:13:39 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i</link>
      <guid>https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  ML on the digital twin: export, train pilots, and import predictions back into the app
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 9 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;Part 8 on dev.to — BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)&lt;/a&gt; · &lt;a href="//08_bi_without_mystery_dimensions_facts_power_bi.md"&gt;repo draft&lt;/a&gt; gave you a &lt;strong&gt;star-shaped&lt;/strong&gt; analytical surface in PostgreSQL/TimescaleDB. Machine learning needs the same history in &lt;strong&gt;files&lt;/strong&gt; or &lt;strong&gt;arrays&lt;/strong&gt;, then a path to bring &lt;strong&gt;scores&lt;/strong&gt; back where operators already look: the Phoenix app.&lt;/p&gt;

&lt;p&gt;This post covers &lt;strong&gt;&lt;code&gt;mix export.ml&lt;/code&gt;&lt;/strong&gt;, the &lt;strong&gt;CSV contract&lt;/strong&gt; documented in &lt;code&gt;MLDatasetExport&lt;/code&gt;, &lt;strong&gt;in-Beam pilot trainers&lt;/strong&gt; (&lt;code&gt;mix simulacoes_visuais.ml_train&lt;/code&gt;), &lt;strong&gt;batch import&lt;/strong&gt; of predictions as JSONL, and the &lt;strong&gt;&lt;code&gt;/smart-brewery/ml-predictions&lt;/code&gt;&lt;/strong&gt; LiveView. &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;&lt;strong&gt;Part 10 on dev.to&lt;/strong&gt;&lt;/a&gt; shifts to &lt;strong&gt;message storms&lt;/strong&gt; and back-pressure in the PON engine.&lt;/p&gt;

&lt;p&gt;For a longer Portuguese walkthrough (notebooks, Python sketches), see &lt;a href="//../../artigos/27_guia_pratico_treino_ml_smart_brewery.md"&gt;&lt;code&gt;docs/artigos/27_guia_pratico_treino_ml_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closed loop in three hops
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; — Snapshot telemetry, OEE, anomalies, rule events, dimensions, and optional CAGG slices to a directory of CSVs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train / infer offline&lt;/strong&gt; — Use Elixir pilots for demos, or Python/R/Julia on the same files; produce &lt;strong&gt;JSON Lines&lt;/strong&gt; (one JSON object per row).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import + display&lt;/strong&gt; — &lt;code&gt;mix import.ml.predictions&lt;/code&gt; (alias) bulk-inserts into &lt;strong&gt;&lt;code&gt;ml_predictions&lt;/code&gt;&lt;/strong&gt;; &lt;strong&gt;&lt;code&gt;MlPredictionsLive&lt;/code&gt;&lt;/strong&gt; lists recent rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Training itself is intentionally &lt;strong&gt;out of band&lt;/strong&gt;: the app does not need GPU drivers to be valuable as the &lt;strong&gt;system of record&lt;/strong&gt; for predictions. For &lt;strong&gt;production readiness&lt;/strong&gt; checklists (monitoring, data validation, CI for models), Google’s &lt;em&gt;ML Test Score&lt;/em&gt; rubric is a compact reference (&lt;a href="https://research.google/pubs/pub46555/" rel="noopener noreferrer"&gt;Breck et al., 2017&lt;/a&gt;); this repo implements only a &lt;strong&gt;thin&lt;/strong&gt; slice—export/import contracts and a LiveView reader—not full MLOps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: &lt;code&gt;mix export.ml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;apps/simulacoes_visuais&lt;/code&gt;, with &lt;strong&gt;&lt;code&gt;:tsdb_enabled&lt;/code&gt;&lt;/strong&gt; and migrations applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix export.ml &lt;span class="nt"&gt;--out&lt;/span&gt; /tmp/ml_export &lt;span class="nt"&gt;--since-hours&lt;/span&gt; 168
&lt;span class="c"&gt;# Long form: mix simulacoes_visuais.export_ml --out /tmp/ml_export --since-hours 72&lt;/span&gt;
&lt;span class="c"&gt;# Skip some CAGGs if missing: --no-cagg or --no-cagg-1h-1day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task delegates to &lt;strong&gt;&lt;code&gt;SimulacoesVisuais.MLDatasetExport.export_all/2&lt;/code&gt;&lt;/strong&gt;, which writes UTF-8 CSVs with headers. The module’s moduledoc is the &lt;strong&gt;contract&lt;/strong&gt; for downstream notebooks—example excerpts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.MLDatasetExport @moduledoc (SQL shapes, excerpt)&lt;/span&gt;
&lt;span class="c1"&gt;# Telemetry (multivariate series)&lt;/span&gt;
&lt;span class="c1"&gt;#   SELECT ts, fact_name, value_float, value_int, value_str, ...&lt;/span&gt;
&lt;span class="c1"&gt;#   FROM telemetry_events WHERE ts &amp;gt;= $1 ORDER BY ts ASC;&lt;/span&gt;
&lt;span class="c1"&gt;# OEE (regression target)&lt;/span&gt;
&lt;span class="c1"&gt;#   SELECT ts, oee_pct, availability_pct, performance_pct, quality_pct, ...&lt;/span&gt;
&lt;span class="c1"&gt;#   FROM oee_snapshots WHERE ts &amp;gt;= $1 ORDER BY ts ASC;&lt;/span&gt;
&lt;span class="c1"&gt;# Rule firings (discrete events)&lt;/span&gt;
&lt;span class="c1"&gt;#   SELECT ts, regra_id, case_id, ... FROM rule_events WHERE ts &amp;gt;= $1 ...&lt;/span&gt;
&lt;span class="c1"&gt;# Dimensions + telemetry_events_1min / _1h / _1day — see source for full SQL.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical filenames include &lt;code&gt;telemetry_events.csv&lt;/code&gt;, &lt;code&gt;oee_snapshots.csv&lt;/code&gt;, &lt;code&gt;rule_events.csv&lt;/code&gt;, &lt;code&gt;dim_equipamento_fbe.csv&lt;/code&gt;, and CAGG exports—matching the README’s &lt;strong&gt;headless simulation&lt;/strong&gt; story (Docker Postgres, Monte Carlo, then export).&lt;/p&gt;

&lt;p&gt;The same migration that introduced &lt;strong&gt;&lt;code&gt;ml_predictions&lt;/code&gt;&lt;/strong&gt; also adds &lt;strong&gt;&lt;code&gt;case_id&lt;/code&gt;&lt;/strong&gt; to &lt;strong&gt;&lt;code&gt;rule_events&lt;/code&gt;&lt;/strong&gt;, so exported rule traces can align with &lt;strong&gt;process-mining&lt;/strong&gt; style case identifiers when you join predictions back to operational event logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2a: Elixir pilots (Scholar / Axon)
&lt;/h2&gt;

&lt;p&gt;For reproducible demos without leaving the BEAM, &lt;strong&gt;&lt;code&gt;mix simulacoes_visuais.ml_train&lt;/code&gt;&lt;/strong&gt; reads the same CSV directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix simulacoes_visuais.ml_train &lt;span class="nt"&gt;--dir&lt;/span&gt; /tmp/ml_export &lt;span class="nt"&gt;--pilot&lt;/span&gt; oee
mix simulacoes_visuais.ml_train &lt;span class="nt"&gt;--dir&lt;/span&gt; /tmp/ml_export &lt;span class="nt"&gt;--pilot&lt;/span&gt; fermentation &lt;span class="nt"&gt;--epochs&lt;/span&gt; 40
mix simulacoes_visuais.ml_train &lt;span class="nt"&gt;--dir&lt;/span&gt; /tmp/ml_export &lt;span class="nt"&gt;--pilot&lt;/span&gt; anomaly &lt;span class="nt"&gt;--epochs&lt;/span&gt; 40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;oee&lt;/code&gt;&lt;/strong&gt; — Scholar &lt;strong&gt;linear regression&lt;/strong&gt; baseline on exported OEE series.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fermentation&lt;/code&gt;&lt;/strong&gt; — &lt;strong&gt;Axon MLP&lt;/strong&gt; pilot on fermentation-related signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;anomaly&lt;/code&gt;&lt;/strong&gt; — &lt;strong&gt;Axon autoencoder&lt;/strong&gt; focused on FBE_01-style vibration features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The task prints &lt;strong&gt;metrics&lt;/strong&gt; to the shell; production workflows usually &lt;strong&gt;emit JSONL&lt;/strong&gt; from a separate inference job and import below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2b: JSONL format for import
&lt;/h2&gt;

&lt;p&gt;Each line is a JSON object. &lt;strong&gt;&lt;code&gt;model_name&lt;/code&gt;&lt;/strong&gt; is required; &lt;strong&gt;&lt;code&gt;ts&lt;/code&gt;&lt;/strong&gt; is optional (defaults to “now” in UTC, normalized to microsecond precision for Ecto).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"model_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"oee_linear_v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2025-03-20T12:00:00.000000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"target_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"oee_pct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"value_float"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;87.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"rmse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;2.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"export_window_h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional fields: &lt;strong&gt;&lt;code&gt;target_name&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;value_float&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;metadata&lt;/code&gt;&lt;/strong&gt; (object, stored as JSON in Postgres).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: persist predictions — schema and context
&lt;/h2&gt;

&lt;p&gt;The migration creates a narrow &lt;strong&gt;fact table&lt;/strong&gt; for batch scores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# priv/repo/migrations/20260319120000_add_rule_events_case_id_and_ml_predictions.exs (excerpt)&lt;/span&gt;
&lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ml_predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:binary_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:utc_datetime_usec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:target_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:value_float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:map&lt;/span&gt;
  &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="ss"&gt;:utc_datetime_usec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ml_predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ts&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ml_predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:ts&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.MlPrediction (schema excerpt)&lt;/span&gt;
&lt;span class="nv"&gt;@primary_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:binary_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;autogenerate:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="s2"&gt;"ml_predictions"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:utc_datetime_usec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:target_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:value_float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="ss"&gt;:utc_datetime_usec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bulk insert from decoded JSON maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.MlPredictions.insert_from_decoded_maps/1 (excerpt)&lt;/span&gt;
&lt;span class="c1"&gt;# Each map must include "model_name"; missing model raises ArgumentError.&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;%{&lt;/span&gt;
      &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="no"&gt;Ecto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="ss"&gt;ts:&lt;/span&gt; &lt;span class="n"&gt;parse_ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;model_name:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"model_name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;target_name:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"target_name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;value_float:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"value_float"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;metadata:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"metadata"&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="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;MlPrediction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mix task: import from disk
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mix simulacoes_visuais.ml_import_predictions @moduledoc (usage)&lt;/span&gt;
&lt;span class="c1"&gt;#   mix simulacoes_visuais.ml_import_predictions --file /path/to/preds.jsonl&lt;/span&gt;
&lt;span class="c1"&gt;# Alias in mix.exs: mix import.ml.predictions --file /path/to/preds.jsonl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task streams the file, &lt;strong&gt;&lt;code&gt;Jason.decode!/1&lt;/code&gt;&lt;/strong&gt; per line, and calls &lt;strong&gt;&lt;code&gt;MlPredictions.insert_from_decoded_maps/1&lt;/code&gt;&lt;/strong&gt;. It requires &lt;strong&gt;&lt;code&gt;:tsdb_enabled&lt;/code&gt;&lt;/strong&gt; so &lt;code&gt;Repo&lt;/code&gt; is part of the running app configuration you expect in TSDB workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  LiveView: &lt;code&gt;/smart-brewery/ml-predictions&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuaisWeb.MlPredictionsLive — mount/3 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tsdb?&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_predictions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;predictions:&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tsdb_enabled:&lt;/span&gt; &lt;span class="n"&gt;tsdb?&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;load_predictions&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;tsdb?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:simulacoes_visuais&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tsdb_enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tsdb?&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MlPredictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_recent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tsdb?&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Router (already introduced in &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="s2"&gt;"/smart-brewery/ml-predictions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;MlPredictionsLive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template surfaces &lt;strong&gt;timestamp&lt;/strong&gt;, &lt;strong&gt;model&lt;/strong&gt;, &lt;strong&gt;target&lt;/strong&gt;, and &lt;strong&gt;value&lt;/strong&gt;, plus a &lt;strong&gt;Refresh&lt;/strong&gt; button—enough to validate that your batch job actually landed in the warehouse.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flow diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph db [TimescaleDB]
    TE[telemetry_events]
    OEE[oee_snapshots]
    RE[rule_events]
    DIM[dimensions]
    MP[ml_predictions]
  end
  subgraph offline [Offline ML]
    CSV[CSV export dir]
    PY[Python / R / etc.]
    JSONL[preds.jsonl]
  end
  subgraph beam [Elixir optional]
    PILOT[ml_train pilots]
  end
  TE --&amp;gt; Export
  OEE --&amp;gt; Export
  RE --&amp;gt; Export
  DIM --&amp;gt; Export
  Export[mix export.ml] --&amp;gt; CSV
  CSV --&amp;gt; PY
  CSV --&amp;gt; PILOT
  PY --&amp;gt; JSONL
  Import[mix import.ml.predictions] --&amp;gt; MP
  JSONL --&amp;gt; Import
  MP --&amp;gt; LV[MlPredictionsLive]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;&lt;strong&gt;Part 7 on dev.to&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;&lt;strong&gt;Part 8 on dev.to&lt;/strong&gt;&lt;/a&gt; built and modeled &lt;strong&gt;historical plant data&lt;/strong&gt;. &lt;strong&gt;This post&lt;/strong&gt; completes the ML ergonomics: &lt;strong&gt;standard CSV exports&lt;/strong&gt;, &lt;strong&gt;documented SQL&lt;/strong&gt;, &lt;strong&gt;optional BEAM-native pilots&lt;/strong&gt;, a &lt;strong&gt;simple import contract&lt;/strong&gt;, and a &lt;strong&gt;LiveView&lt;/strong&gt; to read &lt;code&gt;ml_predictions&lt;/code&gt;. Next: keeping the &lt;strong&gt;notification graph&lt;/strong&gt; healthy when rates spike (&lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;&lt;strong&gt;Part 10 on dev.to&lt;/strong&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breck et al. (2017)&lt;/strong&gt; — &lt;em&gt;The ML Test Score&lt;/em&gt; — &lt;a href="https://research.google/pubs/pub46555/" rel="noopener noreferrer"&gt;Google Research&lt;/a&gt; (production-readiness rubric; not a tutorial).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axon / Scholar&lt;/strong&gt; — neural and classical ML in Elixir — &lt;a href="https://hexdocs.pm/axon" rel="noopener noreferrer"&gt;hexdocs.pm/axon&lt;/a&gt;, &lt;a href="https://hexdocs.pm/scholar" rel="noopener noreferrer"&gt;hexdocs.pm/scholar&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo (PT)&lt;/strong&gt; — &lt;a href="//../../artigos/27_guia_pratico_treino_ml_smart_brewery.md"&gt;&lt;code&gt;docs/artigos/27_guia_pratico_treino_ml_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo (code)&lt;/strong&gt; — &lt;a href="//../../apps/simulacoes_visuais/mix/tasks/simulacoes_visuais.export_ml.ex"&gt;&lt;code&gt;export_ml&lt;/code&gt; task&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/ml_dataset_export.ex"&gt;&lt;code&gt;ml_dataset_export.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/mix/tasks/simulacoes_visuais.ml_train.ex"&gt;&lt;code&gt;ml_train&lt;/code&gt; task&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/mix/tasks/simulacoes_visuais.ml_import_predictions.ex"&gt;&lt;code&gt;ml_import_predictions&lt;/code&gt; task&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/ml_predictions.ex"&gt;&lt;code&gt;ml_predictions.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais_web/live/ml_predictions_live.ex"&gt;&lt;code&gt;ml_predictions_live.ex&lt;/code&gt;&lt;/a&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;ML on the digital twin: export, train pilots, and import predictions back into the app&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;Part 8 on dev.to — BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)&lt;/a&gt; · &lt;a href="//08_bi_without_mystery_dimensions_facts_power_bi.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;Part 10 on dev.to — When notifications explode: message storms, deduplication, and back-pressure in PON&lt;/a&gt; · &lt;a href="//10_when_notifications_explode_message_storms_pon.md"&gt;repo draft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>machinelearning</category>
      <category>timescaledb</category>
      <category>architecture</category>
    </item>
    <item>
      <title>BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:09:28 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj</link>
      <guid>https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 8 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;Part 7 on dev.to — From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB&lt;/a&gt; · &lt;a href="//07_from_simulation_to_storage_telemetry_broadway_timescaledb.md"&gt;repo draft&lt;/a&gt; landed &lt;strong&gt;raw&lt;/strong&gt; rows in &lt;code&gt;telemetry_events&lt;/code&gt; and sibling tables for OEE, anomalies, and rule firings. Analysts rarely want fifty-seven wide columns per millisecond; they want a &lt;strong&gt;model&lt;/strong&gt; they can relate in a semantic layer and refresh on a schedule—or query live with guardrails.&lt;/p&gt;

&lt;p&gt;This post describes the &lt;strong&gt;star-style&lt;/strong&gt; objects in the Smart Brewery Postgres/Timescale database, how &lt;strong&gt;&lt;code&gt;SimulacoesVisuais.SmartBreweryBI&lt;/code&gt;&lt;/strong&gt; mirrors those queries for the in-app &lt;strong&gt;BI&lt;/strong&gt; tab, and how &lt;strong&gt;Power BI&lt;/strong&gt; (or any SQL BI tool) can connect safely. &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;&lt;strong&gt;Part 9 on dev.to&lt;/strong&gt;&lt;/a&gt; picks up &lt;strong&gt;ML exports and prediction round-trips&lt;/strong&gt;. The dimensional vocabulary—&lt;strong&gt;facts&lt;/strong&gt; vs &lt;strong&gt;dimensions&lt;/strong&gt;, conformed keys, slowly changing attributes—follows the Kimball-style methodology (&lt;em&gt;The Data Warehouse Toolkit&lt;/em&gt;, 3rd ed., Wiley); our schema is a pragmatic subset for a twin demo, not a full enterprise bus matrix.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not query only &lt;code&gt;telemetry_events&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;The hypertable is the source of truth for &lt;strong&gt;high-volume&lt;/strong&gt; numeric telemetry. For dashboards you usually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bucket&lt;/strong&gt; time with &lt;code&gt;time_bucket&lt;/code&gt; (TimescaleDB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate&lt;/strong&gt; (&lt;code&gt;avg&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;) per bucket and signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join&lt;/strong&gt; to &lt;strong&gt;dimensions&lt;/strong&gt; so charts show “Fermentador A” instead of &lt;code&gt;fbe_06_internal_temp&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Continuous aggregates (&lt;code&gt;telemetry_events_1min&lt;/code&gt;, &lt;code&gt;_1h&lt;/code&gt;, &lt;code&gt;_1day&lt;/code&gt;) precompute that roll-up. Views on top expose a &lt;strong&gt;fact&lt;/strong&gt; shape with &lt;code&gt;fbe_id&lt;/code&gt; and &lt;code&gt;fact_name&lt;/code&gt; ready for foreign-key-style relationships in Power BI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dimension tables: equipment and variables
&lt;/h2&gt;

&lt;p&gt;The migration seeds two conformed dimensions used across reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# priv/repo/migrations/20250319200000_add_star_schema_dimensions_and_fact_views.exs (excerpt)&lt;/span&gt;
&lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:dim_equipamento_fbe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:fbe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:fase_operacional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# INSERT … FBE_01 … FBE_11 with Portuguese operational labels&lt;/span&gt;

&lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:dim_variaveis_mapeamento&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:fact_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:descricao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:unidade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# INSERT … maps each fbe_XX_* fact atom string to human description + unit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A later migration adds &lt;strong&gt;&lt;code&gt;descricao_longa&lt;/code&gt;&lt;/strong&gt; (aligned with &lt;code&gt;FatoDescriptions&lt;/code&gt;) and &lt;strong&gt;&lt;code&gt;dim_regras&lt;/code&gt;&lt;/strong&gt;: metadata for PON rules (&lt;code&gt;r_01&lt;/code&gt; … &lt;code&gt;r_12&lt;/code&gt;) so &lt;code&gt;rule_events&lt;/code&gt; can be explained in plain language—see &lt;a href="//../../apps/simulacoes_visuais/priv/repo/migrations/20260320140000_power_bi_dim_context.exs"&gt;&lt;code&gt;20260320140000_power_bi_dim_context.exs&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Calendar dimension and fact views
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dim_calendario&lt;/code&gt; is built from &lt;strong&gt;distinct buckets&lt;/strong&gt; of the 1-minute continuous aggregate—useful for time-intelligence style filters without scanning the full hypertable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- From the same migration (conceptual shape)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;dim_calendario&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
  &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ts_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;YEAR&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MONTH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;telemetry_events_1min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fact views project CAGG columns into a stable star join key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;fact_telemetria_agregada_1min&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ts_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;UPPER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fact_name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fbe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;fact_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;value_float_avg&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;value_float_min&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;min_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;value_float_max&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;telemetry_events_1min&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parallel views exist for &lt;strong&gt;&lt;code&gt;_1h&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;_1day&lt;/code&gt;&lt;/strong&gt; grains. In Power BI, typical relationships are: fact → &lt;code&gt;dim_equipamento_fbe&lt;/code&gt; on &lt;code&gt;fbe_id&lt;/code&gt;, fact → &lt;code&gt;dim_variaveis_mapeamento&lt;/code&gt; on &lt;code&gt;fact_name&lt;/code&gt;, fact → &lt;code&gt;dim_calendario&lt;/code&gt; on &lt;code&gt;ts_bucket&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAGG latency vs freshness:&lt;/strong&gt; Continuous aggregates refresh on a policy schedule (see migrations under &lt;code&gt;priv/repo/migrations&lt;/code&gt; for &lt;code&gt;telemetry_events_1min&lt;/code&gt; and friends). That is ideal for &lt;strong&gt;import&lt;/strong&gt; or &lt;strong&gt;DirectQuery&lt;/strong&gt; models that scan fewer rows per question. When operators need charts that track the last minutes without waiting for the next rollup refresh, &lt;code&gt;SmartBreweryBI&lt;/code&gt; hits &lt;strong&gt;&lt;code&gt;telemetry_events&lt;/code&gt;&lt;/strong&gt; directly with &lt;code&gt;time_bucket&lt;/code&gt;—same SQL idioms, different freshness/cost trade-off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical note:&lt;/strong&gt; &lt;code&gt;rule_events.regra_id&lt;/code&gt; is persisted as the string form of the numeric rule id (e.g. &lt;code&gt;"1"&lt;/code&gt;). &lt;code&gt;dim_regras&lt;/code&gt; uses keys like &lt;code&gt;"r_01"&lt;/code&gt;. For joins you may add a calculated column or a small bridge in SQL—worth standardizing in one place for your deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline analytics:&lt;/strong&gt; &lt;code&gt;mix export.ml&lt;/code&gt; (with TSDB enabled) emits CSV slices of telemetry, OEE, anomalies, rules, and dimension tables—useful for Python/R notebooks or training pipelines before &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;&lt;strong&gt;Part 9 on dev.to&lt;/strong&gt;&lt;/a&gt;’s prediction import path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event facts beside telemetry
&lt;/h2&gt;

&lt;p&gt;Telemetry is only one analytical slice. The same database holds &lt;strong&gt;&lt;code&gt;oee_snapshots&lt;/code&gt;&lt;/strong&gt; (availability / performance / quality components over time), &lt;strong&gt;&lt;code&gt;anomaly_events&lt;/code&gt;&lt;/strong&gt; (EMA-driven highlights aligned with the operator UI), and &lt;strong&gt;&lt;code&gt;rule_events&lt;/code&gt;&lt;/strong&gt; (which PON rule fired, with optional &lt;strong&gt;&lt;code&gt;case_id&lt;/code&gt;&lt;/strong&gt; for scenario tracking). None of these replace the star views—they &lt;strong&gt;complement&lt;/strong&gt; them: a single Power BI report can place OEE line charts next to aggregated temperature trends and a table of recent rule firings, all filtered by the same time slicer on &lt;code&gt;ts&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Native BI in Elixir: &lt;code&gt;SmartBreweryBI&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The Phoenix app does not require Power BI for demos. &lt;strong&gt;&lt;code&gt;SmartBreweryBI&lt;/code&gt;&lt;/strong&gt; centralizes parameterized SQL: OEE cards, telemetry trends with &lt;code&gt;time_bucket&lt;/code&gt;, correlation pivots, synoptic averages by FBE, anomaly Pareto, and rule counts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBreweryBI (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;default_filters&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;%{&lt;/span&gt;
    &lt;span class="s2"&gt;"window"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"24h"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"granularity"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"1h"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"fbe_id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"fact_name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"all"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;dashboard_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;normalize_filters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_to_hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"window"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="p"&gt;%{&lt;/span&gt;
    &lt;span class="ss"&gt;oee_cards:&lt;/span&gt; &lt;span class="n"&gt;oee_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;oee_trend:&lt;/span&gt; &lt;span class="n"&gt;oee_trend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"granularity"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="ss"&gt;telemetry_trend:&lt;/span&gt; &lt;span class="n"&gt;telemetry_trend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;cep_chart:&lt;/span&gt; &lt;span class="n"&gt;cep_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;correlation_points:&lt;/span&gt; &lt;span class="n"&gt;correlation_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;synoptic_status:&lt;/span&gt; &lt;span class="n"&gt;synoptic_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;anomaly_pareto:&lt;/span&gt; &lt;span class="n"&gt;anomaly_pareto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;rule_top:&lt;/span&gt; &lt;span class="n"&gt;rule_top&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;totals:&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;BI&lt;/strong&gt; view mode in &lt;code&gt;SmartBreweryLive&lt;/code&gt; (&lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to&lt;/a&gt;) loads this map when &lt;code&gt;:tsdb_enabled&lt;/code&gt; is true—same warehouse, two consumers (LiveView vs external BI).&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;near-real-time&lt;/strong&gt; trends, the module prefers querying &lt;strong&gt;&lt;code&gt;telemetry_events&lt;/code&gt;&lt;/strong&gt; with &lt;code&gt;time_bucket&lt;/code&gt; for selected panels so results are not delayed by CAGG refresh policies; aggregated &lt;strong&gt;views&lt;/strong&gt; remain the sweet spot for heavier report workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Governance: read-only database role
&lt;/h2&gt;

&lt;p&gt;The migration &lt;strong&gt;&lt;code&gt;AddPowerbiAnalyticsReadonlyRole&lt;/code&gt;&lt;/strong&gt; creates &lt;strong&gt;&lt;code&gt;powerbi_analytics&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;LOGIN&lt;/code&gt;, &lt;code&gt;CONNECT&lt;/code&gt; to the database, &lt;code&gt;USAGE&lt;/code&gt; on &lt;code&gt;public&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/strong&gt; on tables and default privileges for future tables. The BI connector should &lt;strong&gt;not&lt;/strong&gt; use the application superuser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Excerpt from 20250319400000_add_powerbi_analytics_readonly_role.exs&lt;/span&gt;
&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CREATE ROLE powerbi_analytics WITH LOGIN PASSWORD %L'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;escaped&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="s2"&gt;"GRANT USAGE ON SCHEMA public TO powerbi_analytics;"&lt;/span&gt;
&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="s2"&gt;"GRANT SELECT ON ALL TABLES IN SCHEMA public TO powerbi_analytics;"&lt;/span&gt;
&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="s2"&gt;"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powerbi_analytics;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;strong&gt;&lt;code&gt;POWERBI_ANALYTICS_PASSWORD&lt;/code&gt;&lt;/strong&gt; before migrating in real environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optional: push rows to Power BI REST
&lt;/h2&gt;

&lt;p&gt;When Import/DirectQuery is not enough for a “live tile” experiment, &lt;strong&gt;&lt;code&gt;PowerBIPushSink&lt;/code&gt;&lt;/strong&gt; can enqueue rows &lt;strong&gt;after&lt;/strong&gt; they are persisted, throttle HTTP calls, and POST to the &lt;a href="https://learn.microsoft.com/en-us/rest/api/power-bi/push-datasets" rel="noopener noreferrer"&gt;Push datasets API&lt;/a&gt;. It is &lt;strong&gt;off by default&lt;/strong&gt; (&lt;code&gt;:power_bi_push&lt;/code&gt; → &lt;code&gt;enabled: false&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBrewery.PowerBIPushSink — encode_row/2 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;encode_row&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="ss"&gt;ts:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;fact_name:&lt;/span&gt; &lt;span class="n"&gt;fact_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value_float:&lt;/span&gt; &lt;span class="n"&gt;vf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value_int:&lt;/span&gt; &lt;span class="n"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value_str:&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;fact_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fact_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
    &lt;span class="s2"&gt;"ts"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime_to_iso8601&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s2"&gt;"fact_name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fact_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"value_float"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;vf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"value_int"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"value_str"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:include_labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FatoDescriptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descricao_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fact_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"descricao"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operational guidance (latency vs license, DirectQuery vs push) lives in &lt;strong&gt;&lt;a href="//../../docs/power-bi-realtime.md"&gt;&lt;code&gt;docs/power-bi-realtime.md&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; at the repo root.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verify the model: &lt;code&gt;mix verify.bi&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The Mix task runs &lt;strong&gt;&lt;code&gt;SmartBreweryBI.run_query_diagnostics/0&lt;/code&gt;&lt;/strong&gt;: each diagnostic query returns &lt;code&gt;:ok&lt;/code&gt; with a small &lt;code&gt;LIMIT&lt;/code&gt; sample, or an error you can fix before publishing reports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From apps/simulacoes_visuais (TSDB enabled)&lt;/span&gt;
mix verify.bi
&lt;span class="c"&gt;# alias: mix simulacoes_visuais.verify_bi_queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero rows in a sample is still &lt;strong&gt;success&lt;/strong&gt; if your simulation was off—the README stresses distinguishing “query broken” from “empty time window”.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture sketch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph warehouse [Postgres / TimescaleDB]
    TE[telemetry_events]
    CAGG[telemetry_events_1min / 1h / 1day]
    F1[fact_telemetria_agregada_*]
    D1[dim_equipamento_fbe]
    D2[dim_variaveis_mapeamento]
    DC[dim_calendario]
    DR[dim_regras]
    OEE[oee_snapshots]
    RE[rule_events]
  end
  subgraph consumers [Consumers]
    LV[SmartBrewery BI tab]
    PBI[Power BI DirectQuery / Import]
    PUSH[Power BI Push optional]
  end
  TE --&amp;gt; CAGG
  CAGG --&amp;gt; F1
  F1 --&amp;gt; D1
  F1 --&amp;gt; D2
  F1 --&amp;gt; DC
  RE --&amp;gt; DR
  LV --&amp;gt; SmartBreweryBI
  SmartBreweryBI --&amp;gt; TE
  SmartBreweryBI --&amp;gt; OEE
  PBI --&amp;gt; F1
  PBI --&amp;gt; D1
  TE --&amp;gt; PUSH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;&lt;strong&gt;Part 7 on dev.to&lt;/strong&gt;&lt;/a&gt; wrote fast &lt;strong&gt;facts&lt;/strong&gt;; &lt;strong&gt;this post&lt;/strong&gt; makes them &lt;strong&gt;legible&lt;/strong&gt;: conformed dimensions, CAGG-backed fact views, a read-only role, optional push, and a single Elixir module that encodes the same SQL the UI and &lt;code&gt;mix verify.bi&lt;/code&gt; rely on. Next, we close the loop with &lt;strong&gt;ML&lt;/strong&gt; on top of these exports (&lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;&lt;strong&gt;Part 9 on dev.to&lt;/strong&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kimball, R.; Ross, M.&lt;/strong&gt; — &lt;em&gt;The Data Warehouse Toolkit&lt;/em&gt; (3rd ed.) — dimensional modeling, star schema, fact/grain discipline (Wiley).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TimescaleDB&lt;/strong&gt; — continuous aggregates as rollups — &lt;a href="https://docs.timescale.com/use-timescale/latest/continuous-aggregates/" rel="noopener noreferrer"&gt;docs.timescale.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Learn&lt;/strong&gt; — Power BI REST / &lt;a href="https://learn.microsoft.com/en-us/rest/api/power-bi/push-datasets" rel="noopener noreferrer"&gt;push datasets&lt;/a&gt; (see also in-repo &lt;a href="//../../docs/power-bi-realtime.md"&gt;&lt;code&gt;power-bi-realtime.md&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; — roles, &lt;code&gt;GRANT&lt;/code&gt;, least privilege — &lt;a href="https://www.postgresql.org/docs/current/sql-grant.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery_bi.ex"&gt;&lt;code&gt;smart_brewery_bi.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery/power_bi_push_sink.ex"&gt;&lt;code&gt;power_bi_push_sink.ex&lt;/code&gt;&lt;/a&gt;, migrations &lt;a href="//../../apps/simulacoes_visuais/priv/repo/migrations/20250319200000_add_star_schema_dimensions_and_fact_views.exs"&gt;&lt;code&gt;20250319200000_add_star_schema_dimensions_and_fact_views.exs&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/priv/repo/migrations/20250319400000_add_powerbi_analytics_readonly_role.exs"&gt;&lt;code&gt;20250319400000_add_powerbi_analytics_readonly_role.exs&lt;/code&gt;&lt;/a&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;Part 7 on dev.to — From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB&lt;/a&gt; · &lt;a href="//07_from_simulation_to_storage_telemetry_broadway_timescaledb.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;Part 9 on dev.to — ML on the digital twin: export, train pilots, and import predictions back into the app&lt;/a&gt; · &lt;a href="//09_ml_digital_twin_export_train_import_predictions.md"&gt;repo draft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>timescaledb</category>
      <category>analytics</category>
      <category>architecture</category>
    </item>
    <item>
      <title>From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:06:08 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762</link>
      <guid>https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 7 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to — Phoenix LiveView in real time: an operations UI on top of a rules engine&lt;/a&gt; · &lt;a href="//06_phoenix_liveview_operations_ui_rules_engine.md"&gt;repo draft&lt;/a&gt; showed how &lt;strong&gt;LiveView&lt;/strong&gt; stays responsive: batched PubSub messages and a second throttle before touching assigns. This post goes &lt;strong&gt;one layer deeper&lt;/strong&gt;: what happens when those same &lt;code&gt;Fato.atualizar/2&lt;/code&gt; notifications must also become &lt;strong&gt;durable rows&lt;/strong&gt; in PostgreSQL with the &lt;strong&gt;TimescaleDB&lt;/strong&gt; extension—without blocking the rule processes or the UI.&lt;/p&gt;

&lt;p&gt;We focus on the &lt;strong&gt;&lt;code&gt;simulacoes_visuais&lt;/code&gt;&lt;/strong&gt; Phoenix app: a &lt;strong&gt;GenStage&lt;/strong&gt; producer, a &lt;strong&gt;Broadway&lt;/strong&gt; pipeline with configurable batching, an asynchronous &lt;strong&gt;GenServer&lt;/strong&gt; writer, and &lt;strong&gt;hypertables&lt;/strong&gt; with retention. &lt;strong&gt;Dimensional BI and Power BI&lt;/strong&gt; consumption are the subject of &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;&lt;strong&gt;Part 8 on dev.to&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a separate persistence path?
&lt;/h2&gt;

&lt;p&gt;The PON engine (&lt;code&gt;tec0301_pon&lt;/code&gt;) is optimized for &lt;strong&gt;reactive evaluation&lt;/strong&gt;: &lt;code&gt;Registry&lt;/code&gt;, &lt;code&gt;Fato&lt;/code&gt; processes, and rule notifications. Long-running &lt;code&gt;Repo.insert_all/3&lt;/code&gt; calls do not belong on that hot path. The Phoenix application therefore acts as a &lt;strong&gt;sink&lt;/strong&gt;, using the same back-pressure vocabulary as &lt;strong&gt;GenStage&lt;/strong&gt; (demand-driven stages) and &lt;strong&gt;Broadway&lt;/strong&gt; (batched consumers)—see the &lt;a href="https://hexdocs.pm/gen_stage" rel="noopener noreferrer"&gt;GenStage&lt;/a&gt; and &lt;a href="https://hexdocs.pm/broadway" rel="noopener noreferrer"&gt;Broadway&lt;/a&gt; documentation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Subscribe&lt;/strong&gt; to the engine’s notification bus (via &lt;code&gt;SmartBreweryFactBroadcaster&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shape&lt;/strong&gt; traffic with back-pressure-friendly stages (GenStage + Broadway, with a GenServer fallback batcher).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; merged batches to PubSub for subscribers (EMA, OEE, downstream services).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist&lt;/strong&gt; only when &lt;code&gt;:tsdb_enabled&lt;/code&gt; is true, through a &lt;strong&gt;queued async writer&lt;/strong&gt; so database latency does not stall the pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running &lt;code&gt;mix run examples/smart_brewery_simulacao.exs&lt;/code&gt; at the monorepo root still exercises &lt;strong&gt;only&lt;/strong&gt; &lt;code&gt;:tec0301_pon&lt;/code&gt;; it does &lt;strong&gt;not&lt;/strong&gt; fill &lt;code&gt;telemetry_events&lt;/code&gt;. Only this app, with TSDB enabled, owns the warehouse path—see &lt;a href="//../../apps/simulacoes_visuais/README.md"&gt;&lt;code&gt;apps/simulacoes_visuais/README.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Turning the warehouse on: supervision and environment
&lt;/h2&gt;

&lt;p&gt;At startup, &lt;code&gt;SimulacoesVisuais.Application&lt;/code&gt; reads &lt;code&gt;SIMULACOES_TSDB_ENABLED&lt;/code&gt; from the OS environment (falling back to &lt;code&gt;Application.get_env(:simulacoes_visuais, :tsdb_enabled, false)&lt;/code&gt;). When enabled, it starts &lt;strong&gt;&lt;code&gt;Repo&lt;/code&gt;&lt;/strong&gt; plus dedicated writers &lt;strong&gt;before&lt;/strong&gt; the HTTP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# apps/simulacoes_visuais/lib/simulacoes_visuais/application.ex (excerpt)&lt;/span&gt;
&lt;span class="n"&gt;tsdb_children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tsdb_enabled&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryAsyncWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;OeeSnapshotWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;AnomalyEventWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RuleEventWriter&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# … PubSub, bridge, TelemetryPipeline, batchers, Monte Carlo, …&lt;/span&gt;
    &lt;span class="n"&gt;tsdb_children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="no"&gt;SimulacoesVisuaisWeb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Endpoint&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fact telemetry&lt;/strong&gt; flows through &lt;strong&gt;&lt;code&gt;TelemetryPipeline&lt;/code&gt;&lt;/strong&gt; (always started) and reaches &lt;code&gt;TelemetryAsyncWriter&lt;/code&gt; only when the flag is on. &lt;strong&gt;OEE, anomalies, and rule firings&lt;/strong&gt; use separate PubSub topics and writers that also depend on &lt;code&gt;Repo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Convenience in dev: &lt;code&gt;mix dev.tsdb&lt;/code&gt; / &lt;code&gt;iex -S mix dev.tsdb&lt;/code&gt; aligns with the README so Postgres/TimescaleDB and Monte Carlo can run together for local demos.&lt;/p&gt;




&lt;h2&gt;
  
  
  GenStage producer: demand-driven ingest
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SmartBreweryFactBroadcaster&lt;/code&gt; resolves the Broadway producer and sends &lt;code&gt;GenStage.cast(producer_pid, {:event, nome_fato, novo_valor})&lt;/code&gt;. The producer keeps a bounded &lt;strong&gt;queue&lt;/strong&gt;; when the queue is full, the oldest event is dropped—trading completeness for &lt;strong&gt;bounded memory&lt;/strong&gt; under burst load.&lt;/p&gt;

&lt;p&gt;Critically, when Broadway has already requested demand with an empty queue, &lt;code&gt;handle_cast/2&lt;/code&gt; &lt;strong&gt;drains immediately&lt;/strong&gt; so events are not stuck until the next &lt;code&gt;handle_demand&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBrewery.TelemetryProducer (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;queue:&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;max_queue:&lt;/span&gt; &lt;span class="n"&gt;max_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pending_demand:&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;new_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ss"&gt;:queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;max_queue&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="ss"&gt;:value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_dropped&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="ss"&gt;:queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;nome_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="ss"&gt;:queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;nome_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drained_queue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;take_from_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;queue:&lt;/span&gt; &lt;span class="n"&gt;drained_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;pending_demand:&lt;/span&gt; &lt;span class="n"&gt;new_pending&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&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="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;queue:&lt;/span&gt; &lt;span class="n"&gt;new_queue&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the producer PID cannot be resolved, the broadcaster falls back to &lt;strong&gt;&lt;code&gt;SmartBreweryTelemetryBatcher&lt;/code&gt;&lt;/strong&gt;; that path must still call &lt;code&gt;TelemetryAsyncWriter.cast_batch/1&lt;/code&gt; on flush so TSDB does not go silent while PubSub keeps working—a parity fix called out in the batcher’s moduledoc.&lt;/p&gt;




&lt;h2&gt;
  
  
  Broadway: processors, batchers, and one merged batch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;TelemetryPipeline&lt;/code&gt; uses Broadway’s &lt;strong&gt;batcher&lt;/strong&gt; stage to merge many &lt;code&gt;{fact, value}&lt;/code&gt; messages into a &lt;strong&gt;single map&lt;/strong&gt; per batch (last write wins per key), then fan-out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBrewery.TelemetryPipeline (excerpt)&lt;/span&gt;
&lt;span class="no"&gt;Broadway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;producer:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;module:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;TelemetryProducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="ss"&gt;:smart_brewery_telemetry_producer&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="ss"&gt;transformer:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:transform&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="ss"&gt;processors:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;default:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;concurrency:&lt;/span&gt; &lt;span class="n"&gt;processor_concurrency&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
  &lt;span class="ss"&gt;batchers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;default:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="ss"&gt;concurrency:&lt;/span&gt; &lt;span class="n"&gt;batcher_concurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;batch_size:&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;batch_timeout:&lt;/span&gt; &lt;span class="n"&gt;batch_timeout&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_batch_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;merge_message_data&lt;/span&gt;&lt;span class="p"&gt;(%{})&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="ss"&gt;:telemetry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:simulacoes_visuais&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:smart_brewery_telemetry_batcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:flush&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;updates_count:&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;buffer_size_before:&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&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="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"smart_brewery:fatos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;push_ema_numeric_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:simulacoes_visuais&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tsdb_enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryAsyncWriter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;messages&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So each batch: &lt;strong&gt;metrics&lt;/strong&gt; (LiveDashboard can chart flush sizes), &lt;strong&gt;PubSub&lt;/strong&gt; on &lt;code&gt;smart_brewery:fatos&lt;/code&gt;, &lt;strong&gt;EMA&lt;/strong&gt; updates for anomaly heuristics, and optionally &lt;strong&gt;async persist&lt;/strong&gt;. &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to&lt;/a&gt;’s LiveView listens on a &lt;strong&gt;different&lt;/strong&gt; topic (&lt;code&gt;smart_brewery:liveview_batch&lt;/code&gt;) fed by &lt;code&gt;LiveViewEventBatcher&lt;/code&gt;—same simulation, two consumption speeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  TelemetryAsyncWriter: decouple DB latency from Broadway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cast_batch/1&lt;/code&gt; enqueues whole batches; a bounded queue drops the &lt;strong&gt;oldest&lt;/strong&gt; batch when overloaded, keeping &lt;strong&gt;recent&lt;/strong&gt; telemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBrewery.TelemetryAsyncWriter (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;cast_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:flush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;queue:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="ss"&gt;:flush&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="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;queue:&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;changesets_from_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&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;rows&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PowerBIPushSink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Broadway never waits on PostgreSQL; the writer serializes inserts and can hook optional push sinks for external analytics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Row shape: from Elixir values to columns
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;TelemetryEvent.changesets_from_batch/2&lt;/code&gt; maps &lt;code&gt;{atom_name, value}&lt;/code&gt; tuples into flat maps suitable for &lt;code&gt;insert_all/3&lt;/code&gt;—numbers as &lt;code&gt;value_float&lt;/code&gt;, booleans split across &lt;code&gt;value_int&lt;/code&gt; / &lt;code&gt;value_str&lt;/code&gt;, other atoms as strings—so continuous aggregates and ML exports can choose the column that fits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.TelemetryEvent (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;to_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;%{&lt;/span&gt;
    &lt;span class="ss"&gt;ts:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;fact_name:&lt;/span&gt; &lt;span class="no"&gt;Atom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;value_float:&lt;/span&gt; &lt;span class="n"&gt;to_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;value_int:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;value_str:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;inserted_at:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;updated_at:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Rule events: same bus, different writer
&lt;/h2&gt;

&lt;p&gt;Rule firings are published to &lt;code&gt;smart_brewery:regras&lt;/code&gt; (for LiveView and others). &lt;strong&gt;&lt;code&gt;RuleEventWriter&lt;/code&gt;&lt;/strong&gt; subscribes, buffers up to a max pending count, and &lt;strong&gt;&lt;code&gt;insert_all&lt;/code&gt;&lt;/strong&gt; into &lt;code&gt;rule_events&lt;/code&gt; in chunks—again isolating database work from the rule processes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBrewery.RuleEventWriter (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:regra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:regra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="c1"&gt;# … trim to max_pending …&lt;/span&gt;
  &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="ss"&gt;:drain&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="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;pending:&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;draining:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OEE snapshots and anomaly rows follow the same pattern (dedicated modules in &lt;code&gt;SmartBrewery.*&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  TimescaleDB: hypertable and retention
&lt;/h2&gt;

&lt;p&gt;The initial migration enables the extension, creates &lt;code&gt;telemetry_events&lt;/code&gt;, and promotes the table to a &lt;strong&gt;hypertable&lt;/strong&gt; partitioned on &lt;code&gt;ts&lt;/code&gt;, with a &lt;strong&gt;7-day retention policy&lt;/strong&gt; on raw rows (adjustable in your deployment):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# priv/repo/migrations/20250318000000_create_telemetry_events.exs (excerpt)&lt;/span&gt;
&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="s2"&gt;"CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;

&lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:telemetry_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:utc_datetime_usec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:fact_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:value_float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:float&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:value_int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:integer&lt;/span&gt;
  &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="ss"&gt;:value_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;
  &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type:&lt;/span&gt; &lt;span class="ss"&gt;:utc_datetime_usec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="s2"&gt;"SELECT create_hypertable('telemetry_events', 'ts', if_not_exists =&amp;gt; true);"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="n"&gt;execute&lt;/span&gt; &lt;span class="s2"&gt;"SELECT add_retention_policy('telemetry_events', INTERVAL '7 days');"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later migrations add &lt;strong&gt;continuous aggregates&lt;/strong&gt; (for example &lt;code&gt;telemetry_events_1min&lt;/code&gt;, hourly and daily rollups) and &lt;strong&gt;compression&lt;/strong&gt; policies on the hypertable. Those objects sit entirely in the database: the Elixir app keeps writing &lt;strong&gt;raw&lt;/strong&gt; &lt;code&gt;telemetry_events&lt;/code&gt; rows; BI queries and &lt;code&gt;mix export.ml&lt;/code&gt; can target either the base table or the CAGGs depending on grain and export flags (&lt;code&gt;--no-cagg&lt;/code&gt;, &lt;code&gt;--no-cagg-1h-1day&lt;/code&gt; in the Mix task help).&lt;/p&gt;




&lt;h2&gt;
  
  
  Fallback batcher and TSDB parity
&lt;/h2&gt;

&lt;p&gt;When the Broadway producer is unavailable, &lt;code&gt;SmartBreweryTelemetryBatcher&lt;/code&gt; still merges updates and broadcasts &lt;code&gt;{:batch, list}&lt;/code&gt; to &lt;code&gt;smart_brewery:fatos&lt;/code&gt;. The flush handler mirrors the pipeline’s TSDB branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:simulacoes_visuais&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tsdb_enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;TelemetryAsyncWriter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, operators would still see PubSub-driven UI signals while &lt;strong&gt;&lt;code&gt;telemetry_events&lt;/code&gt; stayed flat&lt;/strong&gt;—a classic split-brain symptom when debugging ingest.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-end picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph engine [tec0301_pon]
    F[Fato]
  end
  subgraph ingest [simulacoes_visuais ingest]
    FB[SmartBreweryFactBroadcaster]
    P[TelemetryProducer GenStage]
    BW[TelemetryPipeline Broadway]
  end
  subgraph fanout [Fan-out]
    PS[PubSub smart_brewery:fatos]
    EMA[EMA / OEE inputs]
    W[TelemetryAsyncWriter]
  end
  subgraph db [PostgreSQL + TimescaleDB]
    T[(telemetry_events hypertable)]
    R[(rule_events etc.)]
  end
  F --&amp;gt; FB
  FB --&amp;gt;|GenStage.cast| P
  P --&amp;gt; BW
  BW --&amp;gt; PS
  BW --&amp;gt; EMA
  BW --&amp;gt;|cast_batch when tsdb_enabled| W
  W --&amp;gt; T
  RN[RegraNotifier] --&amp;gt;|PubSub regras| RW[RuleEventWriter]
  RW --&amp;gt; R
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Operating and verifying
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mix verify.tsdb&lt;/code&gt;&lt;/strong&gt; — checks extension, row counts, recent timestamps, and whether the Broadway producer and writers are alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless simulation&lt;/strong&gt; — &lt;code&gt;SIMULACOES_TSDB_ENABLED=true AUTO_START_MONTE_CARLO=true mix phx.server&lt;/code&gt; (from the app directory) populates tables for ML export without opening the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention&lt;/strong&gt; — &lt;code&gt;mix simulacoes_visuais.retention --days N&lt;/code&gt; (from the &lt;code&gt;apps/simulacoes_visuais&lt;/code&gt; directory, TSDB enabled) replaces the hypertable retention policy without editing migrations—the default migration still installs a 7-day window.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;&lt;strong&gt;Part 6 on dev.to&lt;/strong&gt;&lt;/a&gt; optimized &lt;strong&gt;perception&lt;/strong&gt; (UI). &lt;strong&gt;This post&lt;/strong&gt; optimizes &lt;strong&gt;durability&lt;/strong&gt;: GenStage demand, Broadway batching, bounded queues, async &lt;code&gt;insert_all&lt;/code&gt;, and a time-series physical model. The engine stays reactive; the warehouse absorbs load on its own terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broadway&lt;/strong&gt; — batching, processors — &lt;a href="https://hexdocs.pm/broadway" rel="noopener noreferrer"&gt;hexdocs.pm/broadway&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenStage&lt;/strong&gt; — producers/consumers, demand — &lt;a href="https://hexdocs.pm/gen_stage" rel="noopener noreferrer"&gt;hexdocs.pm/gen_stage&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TimescaleDB&lt;/strong&gt; — hypertables, continuous aggregates, retention — &lt;a href="https://docs.timescale.com/" rel="noopener noreferrer"&gt;docs.timescale.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry&lt;/strong&gt; — &lt;code&gt;telemetry&lt;/code&gt; events in BEAM apps — &lt;a href="https://hexdocs.pm/telemetry" rel="noopener noreferrer"&gt;hexdocs.pm/telemetry&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/application.ex"&gt;&lt;code&gt;application.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery/telemetry_pipeline.ex"&gt;&lt;code&gt;telemetry_pipeline.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery/telemetry_producer.ex"&gt;&lt;code&gt;telemetry_producer.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery/telemetry_async_writer.ex"&gt;&lt;code&gt;telemetry_async_writer.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery_telemetry_batcher.ex"&gt;&lt;code&gt;smart_brewery_telemetry_batcher.ex&lt;/code&gt;&lt;/a&gt;, migration &lt;a href="//../../apps/simulacoes_visuais/priv/repo/migrations/20250318000000_create_telemetry_events.exs"&gt;&lt;code&gt;20250318000000_create_telemetry_events.exs&lt;/code&gt;&lt;/a&gt;; &lt;a href="//../../performance-dev.md"&gt;&lt;code&gt;docs/performance-dev.md&lt;/code&gt;&lt;/a&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to — Phoenix LiveView in real time: an operations UI on top of a rules engine&lt;/a&gt; · &lt;a href="//06_phoenix_liveview_operations_ui_rules_engine.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;Part 8 on dev.to — BI without mystery: dimensions, facts, and consuming the data (e.g. Power BI)&lt;/a&gt; · &lt;a href="//08_bi_without_mystery_dimensions_facts_power_bi.md"&gt;repo draft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>otp</category>
      <category>timescaledb</category>
      <category>broadway</category>
    </item>
    <item>
      <title>Phoenix LiveView in real time: an operations UI on top of a rules engine</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:02:00 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci</link>
      <guid>https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Phoenix LiveView in real time: an operations UI on top of a rules engine
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 6 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;Part 5 on dev.to — Smart Brewery: a digital twin brewery as a PON lab&lt;/a&gt; · &lt;a href="//05_smart_brewery_digital_twin_pon_lab.md"&gt;repo draft&lt;/a&gt; described the &lt;strong&gt;Smart Brewery&lt;/strong&gt; digital twin: 57 &lt;code&gt;Fato&lt;/code&gt; processes, twelve &lt;code&gt;defrule&lt;/code&gt; modules, scripted &lt;code&gt;simular/0&lt;/code&gt;, and the hybrid &lt;strong&gt;Monte Carlo&lt;/strong&gt; loop. Operators do not introspect the &lt;code&gt;Registry&lt;/code&gt; in &lt;code&gt;iex&lt;/code&gt;; they need a &lt;strong&gt;panel&lt;/strong&gt;. This post walks through &lt;strong&gt;&lt;code&gt;SimulacoesVisuaisWeb.SmartBreweryLive&lt;/code&gt;&lt;/strong&gt;: how Phoenix LiveView &lt;strong&gt;subscribes&lt;/strong&gt; to the same changing world, why we &lt;strong&gt;batch&lt;/strong&gt; updates, and how &lt;strong&gt;rule firings&lt;/strong&gt; surface as log lines and visual hints—without duplicating the persistence pipeline (that is &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;&lt;strong&gt;Part 7 on dev.to&lt;/strong&gt;&lt;/a&gt;). LiveView’s model—server-owned state, incremental patches over a channel—is documented in the official &lt;a href="https://hexdocs.pm/phoenix_live_view/welcome.html" rel="noopener noreferrer"&gt;Phoenix LiveView guides&lt;/a&gt;; batching here is our &lt;strong&gt;application-level back-pressure&lt;/strong&gt; so high telemetry rates do not flood diff generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Route and scope
&lt;/h2&gt;

&lt;p&gt;The SCADA-style dashboard is a standard LiveView route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# apps/simulacoes_visuais/lib/simulacoes_visuais_web/router.ex (excerpt)&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SimulacoesVisuaisWeb&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pipe_through&lt;/span&gt; &lt;span class="ss"&gt;:browser&lt;/span&gt;

  &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="s2"&gt;"/smart-brewery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SmartBreweryLive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;
  &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="s2"&gt;"/smart-brewery/ml-predictions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;MlPredictionsLive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/smart-brewery&lt;/code&gt; is the twin console; &lt;code&gt;/smart-brewery/ml-predictions&lt;/code&gt; previews ML-backed views (&lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;&lt;strong&gt;Part 9 on dev.to&lt;/strong&gt;&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;mount&lt;/code&gt;: snapshot + subscriptions + one stream
&lt;/h2&gt;

&lt;p&gt;On connect, the LiveView builds a map of current fact values (best effort via &lt;code&gt;Fato.obter/1&lt;/code&gt;), subscribes to four &lt;strong&gt;Phoenix PubSub&lt;/strong&gt; topics, and initializes an &lt;strong&gt;event log&lt;/strong&gt; as a LiveView &lt;strong&gt;stream&lt;/strong&gt; (append-only UI list without holding an ever-growing assign).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuaisWeb.SmartBreweryLive — mount/3 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;CaseContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;fatos_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos_names&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;fatos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fatos_names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{},&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;nome&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe_obter_fato&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"smart_brewery:liveview_batch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"smart_brewery:oee"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"smart_brewery:anomalias"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"smart_brewery:regras"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;initial_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sistema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"LiveView conectada. Aguardando notificações PON."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;socket&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;fatos_names:&lt;/span&gt; &lt;span class="n"&gt;fatos_names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;fatos:&lt;/span&gt; &lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:event_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;initial_entry&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;dom_id:&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"log-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real module chains a much larger &lt;code&gt;assign/2&lt;/code&gt; before &lt;code&gt;stream/3&lt;/code&gt; (FBE grouping, &lt;code&gt;view_mode&lt;/code&gt;, OEE, BI filters, &lt;code&gt;pending_fato_updates&lt;/code&gt;, sparklines, TSDB flags, and rule-flash state). The snippet shows the &lt;strong&gt;data path&lt;/strong&gt; that matters here: read facts once, subscribe to four topics, seed the log stream.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;smart_brewery:liveview_batch&lt;/code&gt;&lt;/strong&gt; — batched fact diffs (see below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;smart_brewery:oee&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;smart_brewery:anomalias&lt;/code&gt;&lt;/strong&gt; — operational KPIs and EMA-style anomaly hints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;smart_brewery:regras&lt;/code&gt;&lt;/strong&gt; — “rule R_k fired” style messages for the operator log and FBE highlight.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why a stream for the event log?
&lt;/h3&gt;

&lt;p&gt;The operator log is a rolling tail of structured entries (&lt;code&gt;id&lt;/code&gt;, timestamp, type, message). Appending with a normal assign would mean keeping the full list in memory and sending larger and larger diffs. LiveView &lt;strong&gt;streams&lt;/strong&gt; are a good fit: each new line is &lt;code&gt;stream_insert/3&lt;/code&gt; (and old rows can be pruned to respect &lt;code&gt;@max_log_entries&lt;/code&gt;), so the LiveView process does not accumulate an unbounded list in a single assign. The DOM gets stable &lt;code&gt;id="log-&amp;lt;unique&amp;gt;"&lt;/code&gt; nodes, which also keeps diffing predictable when Monte Carlo is noisy.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;fact table&lt;/strong&gt;, by contrast, is a keyed map (&lt;code&gt;assigns.fatos&lt;/code&gt;) that the template can render by FBE—better for random access and numeric formatting than streaming dozens of independent row ids.&lt;/p&gt;

&lt;h2&gt;
  
  
  From PON notifications to PubSub: &lt;code&gt;SmartBreweryFactBroadcaster&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Tec0301Pon.PON.PubSub&lt;/code&gt; is the &lt;strong&gt;engine&lt;/strong&gt; bus (Parts 2–5). The Phoenix app adds a &lt;strong&gt;bridge&lt;/strong&gt; process that registers on every Smart Brewery fact name and forwards each &lt;code&gt;{:notificacao, name, value}&lt;/code&gt; (or batch map) to telemetry &lt;strong&gt;and&lt;/strong&gt; the UI batcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.SmartBreweryFactBroadcaster — push_telemetry_for_fact/2 (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;push_telemetry_for_fact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# … GenStage.cast to Broadway producer when available, else telemetry batcher fallback …&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:push_liveview_telemetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;LiveViewEventBatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So: &lt;strong&gt;one&lt;/strong&gt; notification fan-out from &lt;code&gt;Fato&lt;/code&gt; still happens inside &lt;code&gt;tec0301_pon&lt;/code&gt;; the &lt;strong&gt;Phoenix&lt;/strong&gt; side adds a subscriber that never blocks the rule processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  First coalescing layer: &lt;code&gt;LiveViewEventBatcher&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Monte Carlo ticks can touch many facts. Broadcasting one PubSub message &lt;strong&gt;per&lt;/strong&gt; &lt;code&gt;Fato.atualizar&lt;/code&gt; would multiply traffic and LiveView work. &lt;code&gt;LiveViewEventBatcher&lt;/code&gt; is a small &lt;code&gt;GenServer&lt;/code&gt; that merges updates into a map and flushes on a &lt;strong&gt;time window&lt;/strong&gt; or &lt;strong&gt;max buffer size&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SimulacoesVisuais.LiveViewEventBatcher (excerpt)&lt;/span&gt;
&lt;span class="nv"&gt;@topic&lt;/span&gt; &lt;span class="s2"&gt;"smart_brewery:liveview_batch"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;GenServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome_do_fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:flush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;buffer:&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;%{}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;@topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;buffer:&lt;/span&gt; &lt;span class="p"&gt;%{},&lt;/span&gt; &lt;span class="ss"&gt;timer_ref:&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscribers receive &lt;strong&gt;&lt;code&gt;{:batch, [{fact, value}, ...]}&lt;/code&gt;&lt;/strong&gt;—one message per window instead of dozens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second coalescing layer: LiveView &lt;code&gt;pending_fato_updates&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Even batched messages can arrive faster than you want to re-render heavy tables. &lt;code&gt;handle_info({:batch, updates}, socket)&lt;/code&gt; merges into &lt;code&gt;assigns.pending_fato_updates&lt;/code&gt; and starts a &lt;strong&gt;single&lt;/strong&gt; &lt;code&gt;Process.send_after&lt;/code&gt; to &lt;code&gt;:flush_pending_fatos&lt;/code&gt; (interval from config, e.g. &lt;code&gt;:smart_brewery_live_flush_pending_ms&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;is_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{},&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;add_pending_and_schedule_flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;add_pending_and_schedule_flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_updates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;when&lt;/span&gt; &lt;span class="n"&gt;new_updates&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;%{}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pending_fato_updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_updates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:pending_fato_updates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&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;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flush_timer_ref&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;socket&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="ss"&gt;:flush_pending_fatos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush_pending_ms&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:flush_timer_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;:flush_pending_fatos&lt;/code&gt; applies the merged map to &lt;code&gt;assigns.fatos&lt;/code&gt;, refreshes in-memory sparklines when TSDB is off, appends a compact log line, and clears the timer—&lt;strong&gt;one&lt;/strong&gt; UI refresh per throttle window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:flush_pending_fatos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pending_fato_updates&lt;/span&gt;

  &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;socket&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:flush_timer_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:pending_fato_updates&lt;/span&gt;&lt;span class="p"&gt;,&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;pending&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;%{}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;new_fatos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;new_spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tsdb_enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sparkline_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sparkline_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sparkline_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fato"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;map_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; atualizações aplicadas"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;socket&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fatos_prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fatos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_fatos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sparkline_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_spark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;append_event_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo also merges &lt;code&gt;pending_anomalia_fbes&lt;/code&gt; into &lt;code&gt;anomalia_fbes&lt;/code&gt; on flush so anomaly badges align with the same frame as the fact map update—same idea: &lt;strong&gt;coalesce&lt;/strong&gt;, then &lt;strong&gt;assign once&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a rule fires: log cooldown and FBE flash
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;Part 5 on dev.to&lt;/a&gt; already described how rule modules notify the Phoenix side; here the LiveView only &lt;strong&gt;consumes&lt;/strong&gt; &lt;code&gt;{:regra, regra_id}&lt;/code&gt; on &lt;code&gt;smart_brewery:regras&lt;/code&gt;. &lt;code&gt;handle_info/2&lt;/code&gt; resolves affected FBEs from &lt;code&gt;@regras_fbe_map&lt;/code&gt;, calls &lt;code&gt;flash_fbes_from_rule/2&lt;/code&gt; for a short highlight (&lt;code&gt;@regra_flash_ms&lt;/code&gt;), and appends to the event log &lt;strong&gt;at most once per cooldown&lt;/strong&gt; per rule id (&lt;code&gt;@regra_log_cooldown_ms&lt;/code&gt;) so chatter does not drown the operator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;:regra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc_now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_regra_log_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;skip?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;@regra_log_cooldown_ms&lt;/span&gt;

  &lt;span class="n"&gt;action_fbes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nv"&gt;@regras_fbe_map&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="p"&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="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

  &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flash_fbes_from_rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_fbes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;skip?&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;socket&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"regra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Regra disparada: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;socket&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;append_event_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:last_regra_log_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_regra_log_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regra_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ties the &lt;strong&gt;PON action&lt;/strong&gt; (already executed in the rule process) to &lt;strong&gt;human-readable&lt;/strong&gt; feedback—without re-running conditions in the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  OEE and anomalies (surface only)
&lt;/h2&gt;

&lt;p&gt;Separate PubSub topics keep &lt;strong&gt;slow-moving KPIs&lt;/strong&gt; and &lt;strong&gt;exception paths&lt;/strong&gt; from competing with the high-rate fact batch channel. &lt;code&gt;handle_info({:oee_update, pct, components}, socket)&lt;/code&gt; when &lt;code&gt;components&lt;/code&gt; is a map refreshes &lt;code&gt;assigns.oee_percent&lt;/code&gt; and the component breakdown for the header badges. &lt;code&gt;handle_info({:anomalia, nome_fato, _valor, _ema, _sigma}, socket)&lt;/code&gt; resolves the fact name to an FBE id and queues a short-lived highlight so operators notice drift without staring at raw numbers.&lt;/p&gt;

&lt;p&gt;How OEE is computed inside &lt;code&gt;SimulacoesVisuais.SmartBrewery.OEE&lt;/code&gt;, how EMA control limits are published, and how the same signal feeds TimescaleDB and Broadway producers are intentionally &lt;strong&gt;out of scope&lt;/strong&gt; here—that is &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;&lt;strong&gt;Part 7 on dev.to&lt;/strong&gt;&lt;/a&gt; (pipeline and persistence).&lt;/p&gt;

&lt;h2&gt;
  
  
  Operator controls: scripted run and Monte Carlo
&lt;/h2&gt;

&lt;p&gt;Buttons call plain &lt;code&gt;handle_event&lt;/code&gt; callbacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"run_simulacao"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;lv_pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simular&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:simulacao_concluida&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;simulando:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"start_monte_carlo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBreweryMonteCarlo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;monte_carlo_ativo:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"stop_monte_carlo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;SimulacoesVisuais&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBreweryMonteCarlo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;monte_carlo_ativo:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long work stays out of the LiveView process; completion is a single &lt;code&gt;handle_info&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  View modes and DOM discipline
&lt;/h2&gt;

&lt;p&gt;The LiveView defines four modes (label + user-facing description) used by the tab strip:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tabela&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tabela&lt;/td&gt;
&lt;td&gt;Tabular facts and live values by equipment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;diagramas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Diagramas&lt;/td&gt;
&lt;td&gt;Mermaid diagrams for the process pipeline and rule graph.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vista 3D&lt;/td&gt;
&lt;td&gt;3D scene with &lt;code&gt;phx-hook&lt;/code&gt; for interaction; detailed static pages per FBE.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BI (painel analítico)&lt;/td&gt;
&lt;td&gt;Ecto-backed charts and filters—&lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;&lt;strong&gt;Part 8 on dev.to&lt;/strong&gt;&lt;/a&gt; goes deeper on the analytical model.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the &lt;strong&gt;active&lt;/strong&gt; &lt;code&gt;view_mode&lt;/code&gt; subtree is rendered into the HEEx template, so switching tabs does not leave three heavy panels in the DOM paying LiveView diff cost. That matters because &lt;code&gt;assigns&lt;/code&gt; still carries the full fact map, OEE, BI payloads, and flash state: invisible work is the enemy of smooth Monte Carlo demos on a laptop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph pon [tec0301_pon]
    Fato[Fato]
    Reg[Tec0301Pon_PubSub]
    Regra[Regra]
  end
  subgraph phx [simulacoes_visuais]
    Br[SmartBreweryFactBroadcaster]
    Bat[LiveViewEventBatcher]
    Ps[Phoenix_PubSub]
    LV[SmartBreweryLive]
  end
  Fato --&amp;gt; Reg
  Reg --&amp;gt; Br
  Br --&amp;gt; Bat
  Bat --&amp;gt; Ps
  Ps --&amp;gt;|"smart_brewery:liveview_batch"| LV
  Regra --&amp;gt;|RegraNotifier| Ps
  Ps --&amp;gt;|"smart_brewery:regras"| LV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we defer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Broadway, GenStage producers, &lt;code&gt;RuleEventWriter&lt;/code&gt;, TimescaleDB retention&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;&lt;strong&gt;Part 7 on dev.to&lt;/strong&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BI dimensions, Power BI–style consumption&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/bi-without-mystery-dimensions-facts-and-consuming-the-data-eg-power-bi-54aj"&gt;&lt;strong&gt;Part 8 on dev.to&lt;/strong&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML predictions page&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/ml-on-the-digital-twin-export-train-pilots-and-import-predictions-back-into-the-app-207i"&gt;&lt;strong&gt;Part 9 on dev.to&lt;/strong&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;LiveView is the &lt;strong&gt;operator-facing adapter&lt;/strong&gt; on top of the same PON graph: &lt;strong&gt;bridge&lt;/strong&gt; from engine &lt;code&gt;Registry&lt;/code&gt; to Phoenix PubSub, &lt;strong&gt;batch&lt;/strong&gt; fact fan-out, &lt;strong&gt;throttle&lt;/strong&gt; assigns, &lt;strong&gt;stream&lt;/strong&gt; structured log events, and &lt;strong&gt;surface&lt;/strong&gt; rule/OEE/anomaly channels. The pattern scales to noisy twins—at the cost of two deliberate aggregation layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix LiveView&lt;/strong&gt; — overview, assigns, streams — &lt;a href="https://hexdocs.pm/phoenix_live_view/welcome.html" rel="noopener noreferrer"&gt;hexdocs.pm/phoenix_live_view&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phoenix PubSub&lt;/strong&gt; — broadcast/subscribe — &lt;a href="https://hexdocs.pm/phoenix_pubsub/" rel="noopener noreferrer"&gt;hexdocs.pm/phoenix_pubsub&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elixir &lt;code&gt;Registry&lt;/code&gt;&lt;/strong&gt; — duplicate-key dispatch (engine side) — &lt;a href="https://hexdocs.pm/elixir/Registry.html" rel="noopener noreferrer"&gt;HexDocs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais_web/live/smart_brewery_live.ex"&gt;&lt;code&gt;smart_brewery_live.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery_fact_broadcaster.ex"&gt;&lt;code&gt;smart_brewery_fact_broadcaster.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais/live_view_event_batcher.ex"&gt;&lt;code&gt;live_view_event_batcher.ex&lt;/code&gt;&lt;/a&gt;, &lt;a href="//../../apps/simulacoes_visuais/lib/simulacoes_visuais_web/router.ex"&gt;&lt;code&gt;router.ex&lt;/code&gt;&lt;/a&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Phoenix LiveView in real time: an operations UI on top of a rules engine&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;Part 5 on dev.to — Smart Brewery: a digital twin brewery as a PON lab&lt;/a&gt; · &lt;a href="//05_smart_brewery_digital_twin_pon_lab.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;Part 7 on dev.to — From simulation to storage: telemetry, Broadway/GenStage, and TimescaleDB&lt;/a&gt; · &lt;a href="//07_from_simulation_to_storage_telemetry_broadway_timescaledb.md"&gt;repo draft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>phoenix</category>
      <category>liveview</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Smart Brewery: a digital twin brewery as a PON lab</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:58:56 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf</link>
      <guid>https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Smart Brewery: a digital twin brewery as a PON lab
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 5 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;Part 4 on dev.to — Hexagonal architecture + PON: Ports &amp;amp; Adapters to decouple the engine&lt;/a&gt; · &lt;a href="//04_hexagonal_pon_ports_adapters.md"&gt;repo draft&lt;/a&gt; separated &lt;strong&gt;ports&lt;/strong&gt; from &lt;strong&gt;adapters&lt;/strong&gt; so rule side effects stay testable. This post switches from toy examples to a &lt;strong&gt;single, opinionated lab&lt;/strong&gt;: a &lt;strong&gt;digital twin&lt;/strong&gt; of a brewery line implemented as a &lt;strong&gt;PON graph&lt;/strong&gt;—dozens of facts, cross-equipment rules, scripted scenarios, and a &lt;strong&gt;hybrid simulator&lt;/strong&gt; (physics-ish models + Monte Carlo noise) that pounds the engine the way a real plant telemetry stream would. Industry vocabulary for “twin + physical asset + lifecycle data” was popularized in part by Grieves’s digital-manufacturing framing (often cited as &lt;em&gt;Digital Twin: Manufacturing Excellence Through Virtual Factory Replication&lt;/em&gt;, 2014); our twin is a &lt;strong&gt;software-only&lt;/strong&gt; stress lab, not a certified plant model.&lt;/p&gt;

&lt;p&gt;The code lives in the monorepo: core facts and rules under &lt;code&gt;tec0301_pon&lt;/code&gt;, continuous simulation under the &lt;code&gt;simulacoes_visuais&lt;/code&gt; Phoenix app.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we are simulating
&lt;/h2&gt;

&lt;p&gt;The twin groups &lt;strong&gt;57 facts&lt;/strong&gt; into &lt;strong&gt;eleven functional block elements (FBEs)&lt;/strong&gt;—mill, mash, filter, boil, heat exchanger, two fermenters, packaging, CIP, AMR fleet, and a &lt;strong&gt;smart grid&lt;/strong&gt; slice. &lt;strong&gt;Twelve rules&lt;/strong&gt; (R_01–R_12) encode operational and safety logic: filtration optimization, packaging interlocks, peak-load shifting, ISO 10816-3 mill protection, NR-13 boiler limits, ISA-88 mash–filter coordination, AMR battery policy, and grid resilience—see the module documentation in &lt;code&gt;Tec0301Pon.Examples.SmartBrewery&lt;/code&gt; and the full attribute/rule catalog in &lt;a href="//../../smart-brewery-fatos-regras.md"&gt;&lt;code&gt;docs/smart-brewery-fatos-regras.md&lt;/code&gt;&lt;/a&gt; (repository root &lt;code&gt;docs/&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;FBE&lt;/th&gt;
&lt;th&gt;Name (concept)&lt;/th&gt;
&lt;th&gt;Fact count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;Mill / grist&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;Mash tun&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;Lauter / filter&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;Boil kettle&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;Heat exchanger&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;Fermenter A&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;Fermenter B&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;Bottling line&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09&lt;/td&gt;
&lt;td&gt;CIP&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;AMR fleet&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Smart grid&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total: 57 facts&lt;/strong&gt;, all &lt;strong&gt;atoms&lt;/strong&gt; backed by &lt;code&gt;Tec0301Pon.PON.Fato&lt;/code&gt; processes, with the usual &lt;code&gt;Registry&lt;/code&gt; fan-out from Parts 2–3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrapping the graph
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Tec0301Pon.Examples.SmartBrewery.start_link/0&lt;/code&gt; starts every fact from the initial-value list, then starts each generated rule module (R_01 … R_12):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/examples/smart_brewery.ex (conceptual excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_link&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;@fatos_iniciais&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Regras&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RegraOtimizacaoFiltracao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Regras&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RegraIntertravamentoEnvase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Regras&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RegraSmartGridLoadBalancing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;# ... remaining rules R_04 – R_12&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Phoenix app, a &lt;strong&gt;bridge&lt;/strong&gt; supervisor typically hosts this graph so LiveView and the Monte Carlo loop see running processes (&lt;code&gt;SimulacoesVisuais.Application&lt;/code&gt; children—&lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;&lt;strong&gt;Part 6 on dev.to&lt;/strong&gt;&lt;/a&gt; focuses on the UI).&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules in the wild: R_01 and R_04
&lt;/h2&gt;

&lt;p&gt;Rules use &lt;code&gt;Tec0301Pon.PON.Builder&lt;/code&gt; (&lt;code&gt;defrule&lt;/code&gt;). &lt;strong&gt;R_01&lt;/strong&gt; tightens filtration when differential pressure is high, clarity is poor, and pump speed is aggressive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/examples/smart_brewery_regras.ex (excerpt — R_01)&lt;/span&gt;
&lt;span class="n"&gt;defrule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RegraOtimizacaoFiltracao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_diff_pressure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:fbe_03_wort_clarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:fbe_03_pump_speed&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_diff_pressure&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_wort_clarity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
      &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_pump_speed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FBE_03&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce_pump_10pct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FBE_03&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_rake_position&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RegraNotifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:r_01&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;R_04&lt;/strong&gt; protects the mill using &lt;strong&gt;&lt;code&gt;edge_triggered: true&lt;/code&gt;&lt;/strong&gt; so the action does not re-fire on every notification while the condition stays true (&lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;Part 3 on dev.to&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/examples/smart_brewery_regras.ex (excerpt — R_04)&lt;/span&gt;
&lt;span class="n"&gt;defrule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RegraProtecaoMoinho&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="ss"&gt;:fbe_01_motor_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:fbe_01_vibration_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:fbe_01_hopper_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:fbe_01_motor_rpm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:fbe_01_feed_valve_state&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_vibration_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_vibration_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_motor_temp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_motor_temp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_hopper_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_motor_rpm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
         &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_hopper_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_motor_rpm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="ss"&gt;edge_triggered:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FBE_01&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce_motor_rpm&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;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_vibration_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:fbe_01_vibration_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;FBE_01&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close_feed_valve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Examples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SmartBrewery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;RegraNotifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:r_04&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FBE_XX&lt;/code&gt; modules update facts to represent equipment response; &lt;code&gt;RegraNotifier&lt;/code&gt; feeds &lt;strong&gt;observability&lt;/strong&gt; (PubSub / persistence hooks in the full app).&lt;/p&gt;

&lt;h2&gt;
  
  
  Scripted walkthrough: &lt;code&gt;simular/0&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For demos and tests, &lt;code&gt;SmartBrewery.simular/0&lt;/code&gt; drives a &lt;strong&gt;narrative&lt;/strong&gt; sequence of &lt;code&gt;Fato.atualizar/2&lt;/code&gt; calls to trigger R_01, then R_02, then R_03:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/examples/smart_brewery.ex (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;simular&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_wort_clarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_pump_speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fbe_03_diff_pressure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;152&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fbe_08_capper_jam_sens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fbe_09_cip_pump_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:fbe_11_grid_power_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;inbound adapter&lt;/strong&gt; story from Part 4 in concrete form: something external (here, a script) &lt;strong&gt;pushes&lt;/strong&gt; world state; rules react.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous stress: hybrid Monte Carlo
&lt;/h2&gt;

&lt;p&gt;Long-running exploration uses &lt;code&gt;SimulacoesVisuais.SmartBreweryMonteCarlo&lt;/code&gt;—a &lt;code&gt;GenServer&lt;/code&gt; that schedules &lt;strong&gt;ticks&lt;/strong&gt;. Each tick runs &lt;strong&gt;dedicated models&lt;/strong&gt; for key subsystems, then applies &lt;strong&gt;stochastic updates&lt;/strong&gt; to a subset of remaining facts according to a &lt;strong&gt;schema&lt;/strong&gt; (&lt;code&gt;{:range, min, max}&lt;/code&gt;, &lt;code&gt;{:enum, [...]}&lt;/code&gt;, &lt;code&gt;:bool&lt;/code&gt;). Some continuous variables use a &lt;strong&gt;random walk&lt;/strong&gt; (Gaussian step) instead of i.i.d. uniforms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# apps/simulacoes_visuais/lib/.../smart_brewery_monte_carlo.ex (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;run_tick_pure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;FBE03Darcy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;FBE06Fermentation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;FBE07Fermentation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;FBE08Markov&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;FBE10Markov&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="no"&gt;FBE11SmartGrid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update_fbe03_cholesky&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# pick N fact names from @mc_fact_names, then:&lt;/span&gt;
  &lt;span class="n"&gt;apply_mc_chosen_updates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;apply_mc_chosen_updates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;valor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome&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;valor&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;apply_mc_chosen_updates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheduling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;handle_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;new_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_tick_pure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="ss"&gt;:tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interval_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:noreply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_state&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Facts owned by the dedicated models are &lt;strong&gt;excluded&lt;/strong&gt; from the naive Monte Carlo list (&lt;code&gt;@excluded_from_mc&lt;/code&gt;) so two writers do not fight. The LiveView &lt;strong&gt;Start / Stop Monte Carlo&lt;/strong&gt; buttons call &lt;code&gt;SmartBreweryMonteCarlo.start_loop/0&lt;/code&gt; and &lt;code&gt;stop_loop/0&lt;/code&gt; (&lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to&lt;/a&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph sim [Simulation_tick]
    MC[MonteCarlo_subset]
    M03[FBE03Darcy]
    M06[FBE06Fermentation]
    M08[FBE08Markov]
  end
  Fato[Tec0301Pon_Fato]
  Reg[Registry_PubSub]
  Regra[Regra_processes]
  FBE[FBE_side_effects]
  sim --&amp;gt; Fato
  Fato --&amp;gt; Reg
  Reg --&amp;gt; Regra
  Regra --&amp;gt; FBE
  Regra --&amp;gt; Notif[RegraNotifier]
  subgraph later [Later_posts]
    LV[LiveView]
    TSDB[TSDB_writers]
  end
  Notif -.-&amp;gt; LV
  Notif -.-&amp;gt; TSDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Telemetry, TSDB, and the Phoenix app (teaser)
&lt;/h2&gt;

&lt;p&gt;With &lt;strong&gt;&lt;code&gt;:tsdb_enabled&lt;/code&gt;&lt;/strong&gt;, the &lt;code&gt;simulacoes_visuais&lt;/code&gt; app starts Ecto + &lt;strong&gt;async writers&lt;/strong&gt; (e.g. rule events) fed from the operational pipeline—&lt;a href="https://dev.to/matheuscamarques/from-simulation-to-storage-telemetry-broadwaygenstage-and-timescaledb-762"&gt;&lt;strong&gt;Part 7 on dev.to&lt;/strong&gt;&lt;/a&gt; covers Broadway, batching, and TimescaleDB in depth. &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;&lt;strong&gt;Part 6 on dev.to&lt;/strong&gt;&lt;/a&gt; covers &lt;strong&gt;LiveView&lt;/strong&gt; (&lt;code&gt;SmartBreweryLive&lt;/code&gt;), streams, and operator UX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heads-up:&lt;/strong&gt; a root &lt;code&gt;mix run examples/smart_brewery_simulacao.exs&lt;/code&gt; path may start only &lt;code&gt;:tec0301_pon&lt;/code&gt; and &lt;strong&gt;not&lt;/strong&gt; persist to &lt;code&gt;telemetry_events&lt;/code&gt;; full TSDB behavior requires the Phoenix app with the right config—see &lt;code&gt;apps/simulacoes_visuais/README.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The Smart Brewery twin is a &lt;strong&gt;deliberately large PON graph&lt;/strong&gt;: many facts, cross-FBE rules, FBE modules that close the loop on state, plus &lt;strong&gt;two simulation modes&lt;/strong&gt;—a &lt;strong&gt;scripted&lt;/strong&gt; &lt;code&gt;simular/0&lt;/code&gt; for storytelling and a &lt;strong&gt;hybrid Monte Carlo&lt;/strong&gt; loop for sustained load. It is the same engine you built in Parts 1–4, now exercised like a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grieves, M.&lt;/strong&gt; — &lt;em&gt;Digital Twin: Manufacturing Excellence Through Virtual Factory Replication&lt;/em&gt; (2014 white paper; search institutional copies) — vocabulary for twins in manufacturing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simão et al. (2013)&lt;/strong&gt; — NOP / notification-oriented control — &lt;a href="https://www.scirp.org/journal/paperinformation?paperid=19842" rel="noopener noreferrer"&gt;SCIRP&lt;/a&gt; (conceptual link to PON-style updates).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="//../../smart-brewery-fatos-regras.md"&gt;&lt;code&gt;docs/smart-brewery-fatos-regras.md&lt;/code&gt;&lt;/a&gt;; &lt;code&gt;lib/tec0301_pon/examples/smart_brewery.ex&lt;/code&gt;, &lt;code&gt;smart_brewery_regras.ex&lt;/code&gt;, &lt;code&gt;smart_brewery_fbe.ex&lt;/code&gt;; &lt;code&gt;apps/simulacoes_visuais/lib/simulacoes_visuais/smart_brewery_monte_carlo.ex&lt;/code&gt;; &lt;code&gt;SimulacoesVisuaisWeb.SmartBreweryLive&lt;/code&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;Smart Brewery: a digital twin brewery as a PON lab&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;Part 4 on dev.to — Hexagonal architecture + PON: Ports &amp;amp; Adapters to decouple the engine&lt;/a&gt; · &lt;a href="//04_hexagonal_pon_ports_adapters.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;Part 6 on dev.to — Phoenix LiveView in real time: an operations UI on top of a rules engine&lt;/a&gt; · &lt;a href="//06_phoenix_liveview_operations_ui_rules_engine.md"&gt;repo draft&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>otp</category>
      <category>architecture</category>
      <category>iot</category>
    </item>
    <item>
      <title>Hexagonal architecture + PON: Ports &amp; Adapters to decouple the engine</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:54:03 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54</link>
      <guid>https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Hexagonal architecture + PON: Ports &amp;amp; Adapters to decouple the engine
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 4 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;Part 3 on dev.to — A metaprogrammed DSL: &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; with less PON boilerplate&lt;/a&gt; · &lt;a href="//03_metaprogrammed_dsl_defrule_defpremissa.md"&gt;repo draft&lt;/a&gt; added &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; so you declare &lt;strong&gt;watch / when / do&lt;/strong&gt; instead of hand-written &lt;code&gt;Regra&lt;/code&gt; callbacks. This post draws a &lt;strong&gt;boundary&lt;/strong&gt; between that reactive core and the messy world: databases, HTTP, MQTT, GPIO, SMS APIs. In Elixir, &lt;strong&gt;Ports &amp;amp; Adapters&lt;/strong&gt; map cleanly to &lt;strong&gt;behaviours&lt;/strong&gt; (&lt;code&gt;@callback&lt;/code&gt;) and implementing modules—the same pattern this repository already uses under &lt;code&gt;lib/tec0301_pon/ports/&lt;/code&gt; and &lt;code&gt;lib/tec0301_pon/adapters/&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why hexagonal thinking matters for rules
&lt;/h2&gt;

&lt;p&gt;A rule’s &lt;code&gt;do:&lt;/code&gt; block is the natural place for &lt;strong&gt;side effects&lt;/strong&gt;: sound an alarm, open a valve, enqueue a job. If you import &lt;code&gt;HTTPoison&lt;/code&gt;, &lt;code&gt;GPIO&lt;/code&gt;, or an Ecto repo &lt;strong&gt;inside&lt;/strong&gt; the rule module, the PON graph becomes hard to &lt;strong&gt;test&lt;/strong&gt; without the network, hardware, or database. Worse, you cannot swap implementations between dev, CI, and production without editing rule code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hexagonal architecture&lt;/strong&gt;—as Cockburn described in his &lt;em&gt;ports and adapters&lt;/em&gt; model (&lt;a href="https://alistair.cockburn.us/hexagonal-architecture/" rel="noopener noreferrer"&gt;original article&lt;/a&gt;; see also Fowler’s &lt;a href="https://martinfowler.com/bliki/HexagonalArchitecture.html" rel="noopener noreferrer"&gt;summary&lt;/a&gt;)—keeps the &lt;strong&gt;application core&lt;/strong&gt; ignorant of those technologies. The core defines &lt;strong&gt;ports&lt;/strong&gt;—interfaces for what it needs. &lt;strong&gt;Adapters&lt;/strong&gt; sit outside and satisfy those interfaces with concrete I/O.&lt;/p&gt;

&lt;p&gt;For PON specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inbound (driving):&lt;/strong&gt; anything that &lt;strong&gt;writes facts&lt;/strong&gt;—sensors, Phoenix controllers, LiveView events, cron jobs—eventually calls &lt;code&gt;Fato.atualizar/2&lt;/code&gt; (or your own thin wrapper). &lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;&lt;strong&gt;Part 6 on dev.to&lt;/strong&gt;&lt;/a&gt; revisits UI; here we only name the direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound (driven):&lt;/strong&gt; what &lt;strong&gt;rules trigger&lt;/strong&gt; when conditions hold—notifications, actuators, persistence. This repo’s examples focus on outbound behaviours.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph inbound [Inbound_adapters]
    Sensor[Sensor_or_UI]
  end
  subgraph core [PON_core]
    Fato[Fato]
    Regra[Regra]
  end
  subgraph port_layer [Port_behaviour]
    PortMod[Ports_PredioAtuadores]
  end
  subgraph outbound [Outbound_adapters]
    IO[Adapters_PredioIO]
    RealHW[Real_HVAC_API]
  end
  Sensor --&amp;gt;|"Fato.atualizar"| Fato
  Fato --&amp;gt; Regra
  Regra --&amp;gt;|"calls contract"| PortMod
  PortMod -.-&amp;gt;|"implements"| IO
  IO --&amp;gt; RealHW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ports in Elixir: &lt;code&gt;@callback&lt;/code&gt; modules
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;port&lt;/strong&gt; here is not the OTP &lt;code&gt;Port&lt;/code&gt; for external OS processes—it is a &lt;strong&gt;behaviour contract&lt;/strong&gt;. Example from the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/ports/predio_atuadores.ex (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioAtuadores&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@moduledoc&lt;/span&gt; &lt;span class="sd"&gt;"""
  Porta (Behaviour) para atuadores do prédio inteligente: iluminação, HVAC, porta de segurança.
  """&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;ligar_luz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;desligar_luz&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;ligar_ar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;ventilar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="nv"&gt;@callback&lt;/span&gt; &lt;span class="n"&gt;trancar_porta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another outbound port is &lt;code&gt;Tec0301Pon.Ports.Alarme&lt;/code&gt; with &lt;code&gt;@callback disparar(motivo :: String.t()) :: :ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;core&lt;/strong&gt; depends only on these &lt;strong&gt;function signatures&lt;/strong&gt;, not on SMS gateways or relay boards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapters: &lt;code&gt;@behaviour&lt;/code&gt; + &lt;code&gt;@impl true&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;adapter&lt;/strong&gt; implements the port. The PoC ships IO-based simulations you can replace with real clients later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/adapters/predio_io.ex (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioIO&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@moduledoc&lt;/span&gt; &lt;span class="sd"&gt;"""
  Adaptador de saída para prédio inteligente (iluminação, HVAC, porta) — simulação com IO.
  """&lt;/span&gt;
  &lt;span class="nv"&gt;@behaviour&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioAtuadores&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;ligar_luz&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[Prédio] Iluminação: Luz LIGADA."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ... desligar_luz, ligar_ar, ventilar, trancar_porta&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/adapters/alarme_io.ex (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;AlarmeIO&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@behaviour&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Alarme&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;disparar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;motivo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"[API] SMS/Push: ALARM - &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;motivo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite smoke-checks the adapter against the port (&lt;code&gt;examples_coverage_test.exs&lt;/code&gt; asserts each &lt;code&gt;PredioIO&lt;/code&gt; callback returns &lt;code&gt;:ok&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules that call adapters: &lt;code&gt;PredioInteligente&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Tec0301Pon.Examples.PredioInteligente.Regras&lt;/code&gt; uses the DSL and &lt;strong&gt;aliases concrete adapter modules&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/examples/predio_inteligente_regras.ex (excerpt)&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt;
&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;AlarmeIO&lt;/span&gt;
&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioIO&lt;/span&gt;

&lt;span class="n"&gt;defrule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RegraEmergencia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:predio_alarme_incendio&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:predio_alarme_incendio&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;AlarmeIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disparar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Alarme de incêndio — modo emergência ativado."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:predio_modo_emergencia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&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="n"&gt;defrule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RegraVentilarCO2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:predio_co2_alto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:predio_modo_emergencia&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:predio_co2_alto&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:predio_modo_emergencia&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;PredioIO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ventilar&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;pragmatic&lt;/strong&gt;: the rules are readable, and the &lt;strong&gt;behaviour&lt;/strong&gt; still documents the contract &lt;code&gt;PredioIO&lt;/code&gt; must honor. For &lt;strong&gt;strict&lt;/strong&gt; hexagonal purity, the rule would call only a &lt;strong&gt;port-facing module&lt;/strong&gt; resolved at runtime (next section).&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-shaped indirection: config or facade
&lt;/h2&gt;

&lt;p&gt;To swap adapters in tests or per environment without changing rule source, introduce a &lt;strong&gt;thin facade&lt;/strong&gt; that delegates to a module from config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example pattern (not required by the PoC — add in your app)&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Building&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@predio_mod&lt;/span&gt; &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:predio_atuadores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioIO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;ventilar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;@predio_mod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ventilar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;trancar_porta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;@predio_mod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trancar_porta&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runtime (non-compile) variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Building&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;predio_mod&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:predio_atuadores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioIO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;ventilar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;predio_mod&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ventilar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;config/test.exs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="ss"&gt;:my_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:predio_atuadores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioStub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules then call &lt;code&gt;MyApp.Building.ventilar()&lt;/code&gt; instead of &lt;code&gt;PredioIO.ventilar()&lt;/code&gt;. The &lt;strong&gt;port&lt;/strong&gt; (&lt;code&gt;PredioAtuadores&lt;/code&gt;) is what you &lt;code&gt;@behaviour&lt;/code&gt; in both &lt;code&gt;PredioIO&lt;/code&gt; and &lt;code&gt;PredioStub&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stub adapter for tests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioStub&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nv"&gt;@behaviour&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PredioAtuadores&lt;/span&gt;

  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;ligar_luz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ligar_luz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;desligar_luz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:desligar_luz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;ligar_ar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ligar_ar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;ventilar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ventilar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;trancar_porta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:trancar_porta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Rule actions run in the Regra process — send to a named test process, not self().&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;whereis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:predio_stub_sink&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:predio_stub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register &lt;code&gt;:predio_stub_sink&lt;/code&gt; as the test process (&lt;code&gt;Process.register(self(), :predio_stub_sink)&lt;/code&gt; in setup), fire the graph, then &lt;code&gt;assert_receive {:predio_stub, :ventilar}&lt;/code&gt;. Alternatively assert only on &lt;strong&gt;fact&lt;/strong&gt; values and keep the stub as &lt;code&gt;:ok&lt;/code&gt; no-ops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inbound in three lines
&lt;/h2&gt;

&lt;p&gt;An inbound adapter translates an external event into a fact change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Conceptual sensor → PON&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MqttTempSensor&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ambient_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;temperature_c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;rule graph&lt;/strong&gt; does not know MQTT exists; it only reacts once &lt;code&gt;:ambient_temp&lt;/code&gt; updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we defer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart Brewery&lt;/strong&gt; domain, simulation volume, and telemetry—&lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;&lt;strong&gt;Part 5 on dev.to&lt;/strong&gt;&lt;/a&gt; onward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiveView&lt;/strong&gt; as an inbound adapter—&lt;a href="https://dev.to/matheuscamarques/phoenix-liveview-in-real-time-an-operations-ui-on-top-of-a-rules-engine-17ci"&gt;&lt;strong&gt;Part 6 on dev.to&lt;/strong&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Full &lt;strong&gt;dependency injection&lt;/strong&gt; framework—behaviours + &lt;code&gt;Application.get_env/3&lt;/code&gt; are enough for many Elixir apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;In this repo&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Tec0301Pon.Ports.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@callback&lt;/code&gt; contract for outbound effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adapter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Tec0301Pon.Adapters.*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@behaviour&lt;/code&gt; implementation (IO, HTTP, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rules&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Examples.PredioInteligente.Regras&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DSL; today alias adapters directly; facades + config tighten the hexagon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hexagonal boundaries do not replace PON—they &lt;strong&gt;wrap&lt;/strong&gt; it: facts and rules stay reactive; adapters keep I/O at the edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cockburn, A.&lt;/strong&gt; — &lt;em&gt;Hexagonal architecture&lt;/em&gt; — &lt;a href="https://alistair.cockburn.us/hexagonal-architecture/" rel="noopener noreferrer"&gt;alistair.cockburn.us&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fowler, M.&lt;/strong&gt; — &lt;em&gt;Hexagonal architecture&lt;/em&gt; (bliki) — &lt;a href="https://martinfowler.com/bliki/HexagonalArchitecture.html" rel="noopener noreferrer"&gt;martinfowler.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elixir&lt;/strong&gt; — &lt;a href="https://hexdocs.pm/elixir/Module.html#module-behaviour" rel="noopener noreferrer"&gt;&lt;code&gt;@callback&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://hexdocs.pm/elixir/Module.html#module-behaviour" rel="noopener noreferrer"&gt;&lt;code&gt;@behaviour&lt;/code&gt;&lt;/a&gt; — HexDocs &lt;code&gt;Module&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;code&gt;lib/tec0301_pon/ports/&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/adapters/&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/examples/predio_inteligente_regras.ex&lt;/code&gt;; &lt;code&gt;Tec0301Pon&lt;/code&gt; &lt;code&gt;@moduledoc&lt;/code&gt; in &lt;code&gt;lib/tec0301_pon.ex&lt;/code&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;Hexagonal architecture + PON: Ports &amp;amp; Adapters to decouple the engine&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;Part 3 on dev.to — A metaprogrammed DSL: &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; with less PON boilerplate&lt;/a&gt; · &lt;a href="//03_metaprogrammed_dsl_defrule_defpremissa.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/smart-brewery-a-digital-twin-brewery-as-a-pon-lab-36mf"&gt;Part 5 on dev.to — Smart Brewery: a digital twin brewery as a PON lab&lt;/a&gt; · &lt;a href="//05_smart_brewery_digital_twin_pon_lab.md"&gt;repo draft&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>otp</category>
      <category>architecture</category>
      <category>hexagonal</category>
    </item>
    <item>
      <title>A metaprogrammed DSL: defrule and defpremissa with less PON boilerplate</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:47:59 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909</link>
      <guid>https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  A metaprogrammed DSL: &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; with less PON boilerplate
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 3 of 12&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/notification-oriented-paradigm-pon-in-elixir-why-the-beam-fits-reactive-rules-2p9e"&gt;Part 1 on dev.to — PON in Elixir: why the BEAM fits reactive rules&lt;/a&gt; · &lt;a href="//01_pon_in_elixir_why_beam.md"&gt;repo draft&lt;/a&gt; introduced PON and the raw &lt;code&gt;Fato&lt;/code&gt; / &lt;code&gt;Regra&lt;/code&gt; API. &lt;a href="https://dev.to/matheuscamarques/from-whiteboard-to-code-mapping-facts-rules-and-premises-to-otp-processes-1blb"&gt;Part 2 on dev.to — From whiteboard to code: mapping Facts, Rules, and Premises to OTP processes&lt;/a&gt; · &lt;a href="//02_from_whiteboard_to_code_otp.md"&gt;repo draft&lt;/a&gt; traced notifications through &lt;code&gt;Registry&lt;/code&gt; and &lt;code&gt;GenServer&lt;/code&gt; processes. This post adds the &lt;strong&gt;ergonomic layer&lt;/strong&gt;: macros in &lt;code&gt;Tec0301Pon.PON.Builder&lt;/code&gt; that generate small modules so you write &lt;strong&gt;&lt;code&gt;watch&lt;/code&gt; / &lt;code&gt;when&lt;/code&gt; / &lt;code&gt;do&lt;/code&gt;&lt;/strong&gt; instead of hand-rolling &lt;code&gt;avaliar/1&lt;/code&gt;, &lt;code&gt;executar/1&lt;/code&gt;, and &lt;code&gt;start_link/0&lt;/code&gt; every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why macros here?
&lt;/h2&gt;

&lt;p&gt;In Elixir, a rule backed by &lt;code&gt;Tec0301Pon.PON.Regra&lt;/code&gt; in &lt;strong&gt;module mode&lt;/strong&gt; needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A function that implements &lt;strong&gt;when&lt;/strong&gt; the rule should fire (&lt;code&gt;avaliar/1&lt;/code&gt; over a &lt;code&gt;memoria&lt;/code&gt; map).&lt;/li&gt;
&lt;li&gt;A function that performs the &lt;strong&gt;action&lt;/strong&gt; (&lt;code&gt;executar/1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;start_link/0&lt;/code&gt; that calls &lt;code&gt;Regra.start_link(watched_facts, __MODULE__, ...)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A premise backed by &lt;code&gt;Tec0301Pon.PON.Premissa&lt;/code&gt; needs a &lt;strong&gt;condition&lt;/strong&gt; and a &lt;code&gt;start_link/0&lt;/code&gt; that wires &lt;code&gt;Premissa.start_link/4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is repetitive and easy to get subtly wrong. The Builder macros &lt;strong&gt;expand at compile time&lt;/strong&gt; into &lt;strong&gt;nested modules&lt;/strong&gt; under your namespace (e.g. &lt;code&gt;MyApp.Rules.Cooling&lt;/code&gt; inside &lt;code&gt;MyApp.Rules&lt;/code&gt;), each with the right callbacks and &lt;code&gt;start_link/0&lt;/code&gt;. At runtime you only start processes—no macro cost on the hot path. That split—compile-time code generation vs runtime message handling—is standard Elixir metaprogramming (&lt;code&gt;quote&lt;/code&gt; / &lt;code&gt;unquote&lt;/code&gt;, &lt;code&gt;__CALLER__&lt;/code&gt;); see the official &lt;a href="https://hexdocs.pm/elixir/Macro.html" rel="noopener noreferrer"&gt;&lt;code&gt;Macro&lt;/code&gt;&lt;/a&gt; module and McCord’s &lt;em&gt;Metaprogramming Elixir&lt;/em&gt; (Pragmatic) for the wider pattern language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Rules&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt;
  &lt;span class="c1"&gt;# defpremissa ... and defrule ... live here&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;use Tec0301Pon.PON.Builder&lt;/code&gt; only &lt;strong&gt;imports&lt;/strong&gt; the macro definitions; the heavy lifting is in &lt;code&gt;defrule&lt;/code&gt;, &lt;code&gt;defpremissa&lt;/code&gt;, and friends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;defrule&lt;/code&gt; expands to (conceptually)
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;defrule Name, watch: [...], when: ..., do: ...&lt;/code&gt; becomes a module &lt;code&gt;MyApp.Rules.Name&lt;/code&gt; (name parts are taken from the macro’s identifier and concatenated with the &lt;strong&gt;caller module&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;Roughly, the generated module contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;avaliar(memoria)&lt;/code&gt;&lt;/strong&gt; — body of your &lt;code&gt;when:&lt;/code&gt; clause (injected AST).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;executar(memoria)&lt;/code&gt;&lt;/strong&gt; — your &lt;code&gt;do:&lt;/code&gt; block, &lt;strong&gt;or&lt;/strong&gt; a loop of &lt;code&gt;Task.start(mod, fun, args)&lt;/code&gt; when you use &lt;strong&gt;instigations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;start_link/0&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;Tec0301Pon.PON.Regra.start_link(watched_facts, __MODULE__, opts)&lt;/code&gt; so the rule process uses &lt;strong&gt;this&lt;/strong&gt; module’s &lt;code&gt;avaliar/1&lt;/code&gt; and &lt;code&gt;executar/1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters: because &lt;code&gt;Regra&lt;/code&gt; holds an atom referring to &lt;strong&gt;your&lt;/strong&gt; generated module, you can &lt;strong&gt;upgrade the module&lt;/strong&gt; in a release and change rule logic while facts keep running—hot code swapping is a BEAM feature the DSL is aligned with (operational details stay out of this post).&lt;/p&gt;

&lt;h3&gt;
  
  
  Expansion sketch (not the real quoted AST)
&lt;/h3&gt;

&lt;p&gt;If you wrote &lt;code&gt;defrule Example, watch: [:a], when: memoria[:a] == 1, do: :ok&lt;/code&gt; inside &lt;code&gt;MyApp.Rules&lt;/code&gt;, compile-time expansion conceptually yields something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Conceptual — actual code is built with Macro.quote in Builder&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Rules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Example&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;avaliar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:a&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="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;executar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_link&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Regra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:a&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;__MODULE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;edge_triggered:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;defpremissa&lt;/code&gt; is analogous: a nested module with &lt;code&gt;condicao/1&lt;/code&gt; and &lt;code&gt;start_link/0&lt;/code&gt; calling &lt;code&gt;Premissa.start_link/4&lt;/code&gt;. Inspect expanded code in a scratch module with &lt;code&gt;Macro.expand&lt;/code&gt; if you enjoy reading &lt;code&gt;quote&lt;/code&gt; output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Options worth knowing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;edge_triggered: true&lt;/code&gt;&lt;/strong&gt; — passed through to &lt;code&gt;Regra.start_link/3&lt;/code&gt;; the action runs only on &lt;strong&gt;false → true&lt;/strong&gt; transitions of the condition (see Part 2 / &lt;code&gt;Regra&lt;/code&gt; docs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;do: [instigations: [{Mod, :fun, [args]}, ...]]&lt;/code&gt;&lt;/strong&gt; — instead of an arbitrary block, spawn &lt;strong&gt;Tasks&lt;/strong&gt; to call MFA tuples when the rule fires. Useful to fan out work or call adapters without blocking the rule process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;when:&lt;/code&gt; as a string&lt;/strong&gt; — a separate macro clause compiles to &lt;code&gt;Code.eval_string/2&lt;/code&gt; over &lt;code&gt;memoria: memoria&lt;/code&gt;. That exists for flexibility (e.g. config-driven rules), but &lt;strong&gt;prefer the normal AST &lt;code&gt;when:&lt;/code&gt;&lt;/strong&gt; in application code: eval’d strings are harder to test, refactor, and secure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example with a normal &lt;code&gt;when:&lt;/code&gt; and &lt;code&gt;do:&lt;/code&gt; block (brewery-flavored, same spirit as the project README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Rules&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt;

  &lt;span class="n"&gt;defpremissa&lt;/span&gt; &lt;span class="no"&gt;HighAmbient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ambient_temp&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ambient_temp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;derive:&lt;/span&gt; &lt;span class="ss"&gt;:high_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;criar_fato:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;defrule&lt;/span&gt; &lt;span class="no"&gt;StartCooling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:high_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:compressor_state&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:high_temp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:compressor_state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Actuators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_compressor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Instigations
&lt;/h3&gt;

&lt;p&gt;Pattern borrowed from the test suite (&lt;code&gt;Tec0301Pon.PON.BuilderTest.RegrasInstigations&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Rules&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt;

  &lt;span class="n"&gt;defrule&lt;/span&gt; &lt;span class="no"&gt;NotifyOps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:alarm_level&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:alarm_level&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:critical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;instigations:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:page_on_call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]}]]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the condition holds, the generated &lt;code&gt;executar/1&lt;/code&gt; starts a &lt;code&gt;Task&lt;/code&gt; per &lt;code&gt;{mod, fun, args}&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  String &lt;code&gt;when:&lt;/code&gt; (discouraged for most apps)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;defrule&lt;/span&gt; &lt;span class="no"&gt;ThresholdFromConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sensor_x&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"memoria[:sensor_x] &amp;gt; 10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates &lt;code&gt;avaliar/1&lt;/code&gt; that evaluates the string at runtime. Use sparingly; never feed untrusted input into that string.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;defpremissa&lt;/code&gt; expands to
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;defpremissa Name, watch: [...], when: ..., derive: :fact [, criar_fato: true]&lt;/code&gt; generates a submodule whose &lt;strong&gt;&lt;code&gt;start_link/0&lt;/code&gt;&lt;/strong&gt; calls:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Premissa.start_link(derive_fact, watch_list, &amp;amp;condicao/1, criar_fato_derivado: ...)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;with &lt;strong&gt;&lt;code&gt;condicao/1&lt;/code&gt;&lt;/strong&gt; equal to your &lt;code&gt;when:&lt;/code&gt; body.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;watch&lt;/code&gt;&lt;/strong&gt; — source fact atoms (one or two in the underlying &lt;code&gt;Premissa&lt;/code&gt; API).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;derive&lt;/code&gt;&lt;/strong&gt; — the &lt;strong&gt;derived&lt;/strong&gt; fact name that other rules can watch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;criar_fato: true&lt;/code&gt;&lt;/strong&gt; — if the derived fact process does not exist yet, start it with initial value &lt;code&gt;false&lt;/code&gt; before subscribing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behavior matches Part 2: the premise process &lt;strong&gt;registers&lt;/strong&gt; on the Registry keys for source facts and only calls &lt;code&gt;Fato.atualizar/2&lt;/code&gt; on the derived fact when the &lt;strong&gt;boolean result changes&lt;/strong&gt;, avoiding notification spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: &lt;code&gt;defcondicao&lt;/code&gt; — aggregate premises
&lt;/h2&gt;

&lt;p&gt;When several boolean facts must be combined (AND/OR) before a single downstream rule fires, &lt;strong&gt;&lt;code&gt;defcondicao&lt;/code&gt;&lt;/strong&gt; generates a submodule whose &lt;code&gt;start_link/0&lt;/code&gt; delegates to &lt;code&gt;Tec0301Pon.PON.Condicao&lt;/code&gt; with either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;merge: :all&lt;/code&gt;&lt;/strong&gt; — every watched value must be strictly &lt;code&gt;true&lt;/code&gt; (AND).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;merge: :any&lt;/code&gt;&lt;/strong&gt; — at least one &lt;code&gt;true&lt;/code&gt; (OR).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;when: expr&lt;/code&gt;&lt;/strong&gt; — custom combination over &lt;code&gt;memoria&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Rules&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt;

  &lt;span class="n"&gt;defcondicao&lt;/span&gt; &lt;span class="no"&gt;AlarmArmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:high_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:low_pressure&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;merge:&lt;/span&gt; &lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;derive:&lt;/span&gt; &lt;span class="ss"&gt;:alarm_ready&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;criar_fato:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;defrule&lt;/span&gt; &lt;span class="no"&gt;SoundAlarm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;watch:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:alarm_ready&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;memoria&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:alarm_ready&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Actuators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sound_alarm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot pass &lt;strong&gt;both&lt;/strong&gt; &lt;code&gt;merge:&lt;/code&gt; and &lt;code&gt;when:&lt;/code&gt;; the macro raises at compile time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrap order (still your responsibility)
&lt;/h2&gt;

&lt;p&gt;The DSL does &lt;strong&gt;not&lt;/strong&gt; start your supervision tree for you. A reliable order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Application.ensure_all_started(:tec0301_pon)&lt;/code&gt;&lt;/strong&gt; (or include the app in your release) so ETS and &lt;code&gt;Tec0301Pon.PON.PubSub&lt;/code&gt; exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start every raw fact&lt;/strong&gt; the graph needs with &lt;code&gt;Fato.start_link(name, initial_value)&lt;/code&gt; (source sensors and any derived facts &lt;strong&gt;unless&lt;/strong&gt; &lt;code&gt;criar_fato: true&lt;/code&gt; creates them).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start premises and conditions&lt;/strong&gt; (&lt;code&gt;MyApp.Rules.HighAmbient.start_link/0&lt;/code&gt;, &lt;code&gt;MyApp.Rules.AlarmArmed.start_link/0&lt;/code&gt;, …).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start rules&lt;/strong&gt; (&lt;code&gt;MyApp.Rules.StartCooling.start_link/0&lt;/code&gt;, …).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Integration tests in the repo follow this pattern under &lt;code&gt;Tec0301Pon.PonCase&lt;/code&gt; with optional &lt;code&gt;Tec0301Pon.PON.Service&lt;/code&gt; registration for global statistics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimal end-to-end sketch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_all_started&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:tec0301_pon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;
&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;MyApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Rules&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ambient_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:compressor_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;HighAmbient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;StartCooling&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ambient_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# HighAmbient may set :high_temp; StartCooling may run if compressor is :off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust module names to match your &lt;code&gt;defrule&lt;/code&gt; / &lt;code&gt;defpremissa&lt;/code&gt; identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side effects and Part 4
&lt;/h2&gt;

&lt;p&gt;Examples above call &lt;code&gt;MyApp.Actuators&lt;/code&gt; or &lt;code&gt;MyApp.Notifications&lt;/code&gt; directly inside &lt;code&gt;do:&lt;/code&gt; or instigations. That is clear for a blog post, but in a larger system you will want &lt;strong&gt;ports and adapters&lt;/strong&gt; so the PON core stays testable and swappable. &lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;&lt;strong&gt;Part 4 on dev.to&lt;/strong&gt;&lt;/a&gt; covers hexagonal boundaries around the engine; the macros stay the same—you only change &lt;strong&gt;what&lt;/strong&gt; &lt;code&gt;executar/1&lt;/code&gt; and instigations call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Macro&lt;/th&gt;
&lt;th&gt;Generates&lt;/th&gt;
&lt;th&gt;Runtime process&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;defrule&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;avaliar/1&lt;/code&gt;, &lt;code&gt;executar/1&lt;/code&gt;, &lt;code&gt;start_link/0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Regra&lt;/code&gt; (module mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;defpremissa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;condicao/1&lt;/code&gt;, &lt;code&gt;start_link/0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Premissa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;defcondicao&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;start_link/0&lt;/code&gt; (+ optional &lt;code&gt;combine/1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Condicao&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;Tec0301Pon.PON.Builder&lt;/code&gt; is the compile-time bridge between &lt;strong&gt;declarative&lt;/strong&gt; PON graphs and the &lt;strong&gt;OTP&lt;/strong&gt; mechanics from Part 2. Prefer AST &lt;code&gt;when:&lt;/code&gt; clauses, understand &lt;code&gt;edge_triggered&lt;/code&gt; and instigations, and start facts before premises and rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elixir &lt;code&gt;Macro&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;quote&lt;/code&gt;, &lt;code&gt;unquote&lt;/code&gt;, hygiene — &lt;a href="https://hexdocs.pm/elixir/Macro.html" rel="noopener noreferrer"&gt;HexDocs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;McCord, C.&lt;/strong&gt; — &lt;em&gt;Metaprogramming Elixir&lt;/em&gt; (Pragmatic Bookshelf) — DSLs and &lt;code&gt;use&lt;/code&gt;/&lt;code&gt;__using__&lt;/code&gt; patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elixir &lt;code&gt;Kernel.SpecialForms&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;defmacro&lt;/code&gt; — &lt;a href="https://hexdocs.pm/elixir/Kernel.SpecialForms.html" rel="noopener noreferrer"&gt;HexDocs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;code&gt;lib/tec0301_pon/pon/builder.ex&lt;/code&gt;; &lt;code&gt;mix docs&lt;/code&gt; for &lt;code&gt;Tec0301Pon.PON.Builder&lt;/code&gt;, &lt;code&gt;Regra&lt;/code&gt;, &lt;code&gt;Premissa&lt;/code&gt;, &lt;code&gt;Condicao&lt;/code&gt;. Expanded list: &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;A metaprogrammed DSL: &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; with less PON boilerplate&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/from-whiteboard-to-code-mapping-facts-rules-and-premises-to-otp-processes-1blb"&gt;Part 2 on dev.to — From whiteboard to code: mapping Facts, Rules, and Premises to OTP processes&lt;/a&gt; · &lt;a href="//02_from_whiteboard_to_code_otp.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;Part 4 on dev.to — Hexagonal architecture + PON: Ports &amp;amp; Adapters to decouple the engine&lt;/a&gt; · &lt;a href="//04_hexagonal_pon_ports_adapters.md"&gt;repo draft&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>otp</category>
      <category>metaprogramming</category>
      <category>architecture</category>
    </item>
    <item>
      <title>From whiteboard to code: mapping Facts, Rules, and Premises to OTP processes</title>
      <dc:creator>Matheus de Camargo Marques</dc:creator>
      <pubDate>Fri, 20 Mar 2026 16:39:52 +0000</pubDate>
      <link>https://dev.to/matheuscamarques/from-whiteboard-to-code-mapping-facts-rules-and-premises-to-otp-processes-1blb</link>
      <guid>https://dev.to/matheuscamarques/from-whiteboard-to-code-mapping-facts-rules-and-premises-to-otp-processes-1blb</guid>
      <description>&lt;p&gt;&lt;em&gt;If this helped you, you can &lt;a href="https://dev.to/matheuscamarques/support-with-a-coffee-2oa0"&gt;support the author with a coffee on dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  From whiteboard to code: mapping Facts, Rules, and Premises to OTP processes
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Part 2 of 12&lt;/strong&gt; — In &lt;a href="https://dev.to/matheuscamarques/notification-oriented-paradigm-pon-in-elixir-why-the-beam-fits-reactive-rules-2p9e"&gt;Part 1 on dev.to — PON in Elixir: why the BEAM fits reactive rules&lt;/a&gt; · &lt;a href="//01_pon_in_elixir_why_beam.md"&gt;repo draft&lt;/a&gt; we motivated the &lt;strong&gt;Notification-Oriented Paradigm (PON)&lt;/strong&gt; and showed the public &lt;code&gt;Fato&lt;/code&gt; / &lt;code&gt;Regra&lt;/code&gt; API. This post opens the engine room: &lt;strong&gt;how&lt;/strong&gt; updates become messages, and &lt;strong&gt;how&lt;/strong&gt; rules and premises attach to fact names using OTP building blocks—without yet using the metaprogrammed DSL (&lt;code&gt;defrule&lt;/code&gt; / &lt;code&gt;defpremissa&lt;/code&gt;), which is &lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;&lt;strong&gt;Part 3 on dev.to&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The whiteboard model
&lt;/h2&gt;

&lt;p&gt;On the board you draw &lt;strong&gt;facts&lt;/strong&gt; (ovals), &lt;strong&gt;rules&lt;/strong&gt; (boxes that watch facts), and sometimes &lt;strong&gt;premises&lt;/strong&gt; (derived facts). The arrow is always the same: &lt;em&gt;when this fact changes, wake up everything that cares&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On the BEAM, a practical mapping is (the OTP design space—processes under supervision exchanging messages—is laid out in Armstrong’s thesis and in Cesarini &amp;amp; Thompson’s &lt;em&gt;Programming Erlang&lt;/em&gt;; here we use only a thin slice):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;In &lt;code&gt;tec0301_pon&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fact&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;named&lt;/strong&gt; &lt;code&gt;GenServer&lt;/code&gt; (&lt;code&gt;Tec0301Pon.PON.Fato&lt;/code&gt;) — process name = fact name (atom).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bus&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;duplicate-key&lt;/strong&gt; &lt;code&gt;Registry&lt;/code&gt; named &lt;code&gt;Tec0301Pon.PON.PubSub&lt;/code&gt; — registry key = fact atom; many processes can register under the same key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;GenServer&lt;/code&gt; (&lt;code&gt;Tec0301Pon.PON.Regra&lt;/code&gt;) that &lt;strong&gt;registers&lt;/strong&gt; under each watched fact key and handles &lt;code&gt;{:notificacao, ...}&lt;/code&gt; (and batch variants).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premise&lt;/td&gt;
&lt;td&gt;Another subscriber process (&lt;code&gt;Tec0301Pon.PON.Premissa&lt;/code&gt;) that &lt;strong&gt;writes&lt;/strong&gt; a derived fact when a boolean condition &lt;strong&gt;changes&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph bus [Registry_PubSub]
    topicA[fact_key_A]
  end
  FatoA[Fato_GenServer_A] --&amp;gt;|"dispatch on key A"| topicA
  topicA --&amp;gt;|send| Regra[Regra_process]
  topicA --&amp;gt;|send| Premissa[Premissa_process]
  Premissa --&amp;gt;|Fato.atualizar| FatoB[Fato_derived]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Application startup: ETS + Registry
&lt;/h2&gt;

&lt;p&gt;When the &lt;code&gt;tec0301_pon&lt;/code&gt; application starts, it ensures a shared ETS table for fast fact reads and starts the Registry that acts as the notification bus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/application.ex (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="ss"&gt;:ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_ets!&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="ss"&gt;keys:&lt;/span&gt; &lt;span class="ss"&gt;:duplicate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partitions:&lt;/span&gt; &lt;span class="no"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedulers_online&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;strategy:&lt;/span&gt; &lt;span class="ss"&gt;:one_for_one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="no"&gt;Supervisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;:duplicate&lt;/code&gt; keys?&lt;/strong&gt; Under a unique-key registry, only one process could register per fact name. Here, &lt;em&gt;many&lt;/em&gt; rules (and premises) may watch &lt;code&gt;:temperature&lt;/code&gt;. Duplicate keys mean “topic” semantics: one key, N subscribers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partitions&lt;/strong&gt; spread registry work across schedulers—useful when many processes register and dispatch runs hot (later posts revisit performance).&lt;/p&gt;

&lt;h2&gt;
  
  
  Fact process: store, compare, dispatch
&lt;/h2&gt;

&lt;p&gt;Each fact is a &lt;code&gt;GenServer&lt;/code&gt; registered &lt;strong&gt;both&lt;/strong&gt; as an OTP name (&lt;code&gt;name: nome_do_fato&lt;/code&gt;) and as the &lt;strong&gt;topic&lt;/strong&gt; for subscribers. When you call &lt;code&gt;Fato.atualizar(fact, value)&lt;/code&gt;, the fact process compares the new value to the old with &lt;strong&gt;&lt;code&gt;===&lt;/code&gt;&lt;/strong&gt;. If nothing changed, it does &lt;strong&gt;not&lt;/strong&gt; call &lt;code&gt;Registry.dispatch&lt;/code&gt;—a cheap guard against useless fan-out (&lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;&lt;strong&gt;Part 10 on dev.to&lt;/strong&gt;&lt;/a&gt; goes deeper on storms and batching).&lt;/p&gt;

&lt;p&gt;When the value &lt;strong&gt;does&lt;/strong&gt; change, it updates ETS and dispatches to every &lt;code&gt;{pid, _value}&lt;/code&gt; registered under that fact’s atom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/fato.ex — handle_cast({:atualizar, novo_valor}, estado) (excerpt)&lt;/span&gt;
&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;inscritos&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;inscritos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:notificacao&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;novo_valor&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message shape &lt;code&gt;{:notificacao, nome_fato, novo_valor}&lt;/code&gt; is the contract every subscriber implements in &lt;code&gt;handle_info/2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch path (preview):&lt;/strong&gt; &lt;code&gt;Fato.atualizar_lote/1&lt;/code&gt; funnels through &lt;code&gt;Tec0301Pon.PON.Fanout&lt;/code&gt; and can deliver &lt;code&gt;{:notificacoes_lote, %{fato =&amp;gt; valor, ...}}&lt;/code&gt; so a rule gets &lt;strong&gt;one&lt;/strong&gt; message per burst instead of dozens. Rules handle both shapes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule process: register, seed memory, evaluate
&lt;/h2&gt;

&lt;p&gt;When a rule starts, it &lt;strong&gt;subscribes&lt;/strong&gt; to each watched fact and &lt;strong&gt;seeds&lt;/strong&gt; its local &lt;code&gt;memoria&lt;/code&gt; map by reading current values (&lt;code&gt;Fato.obter/1&lt;/code&gt;), so the first evaluation sees a consistent snapshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tec0301_pon/pon/regra.ex — init/1 (excerpt)&lt;/span&gt;
&lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;fato&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
  &lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PubSub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;memoria_inicial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fatos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{},&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;valor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;obter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fato&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;memoria:&lt;/span&gt; &lt;span class="n"&gt;memoria_inicial&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On each &lt;code&gt;{:notificacao, ...}&lt;/code&gt;, the rule updates &lt;code&gt;memoria&lt;/code&gt;, &lt;strong&gt;drains&lt;/strong&gt; additional notifications already in the mailbox (reducing duplicate work in bursts), then runs the condition and possibly the action. A second clause handles &lt;code&gt;{:notificacoes_lote, updates}&lt;/code&gt; by merging only the keys the rule watches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premise process: same bus, different side effect
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Tec0301Pon.PON.Premissa&lt;/code&gt; is “rule-like” in that it &lt;strong&gt;registers&lt;/strong&gt; on source facts and receives the same notifications. Instead of firing an arbitrary action, it evaluates &lt;code&gt;condicao_fn.(memoria)&lt;/code&gt; and, when the boolean &lt;strong&gt;changes&lt;/strong&gt;, calls &lt;code&gt;Fato.atualizar(nome_fato_derivado, novo_resultado)&lt;/code&gt;. That derived fact becomes another topic: downstream rules only watch the derived name.&lt;/p&gt;

&lt;p&gt;Options include &lt;code&gt;criar_fato_derivado: true&lt;/code&gt;, which starts the derived fact as &lt;code&gt;false&lt;/code&gt; if it did not exist—handy in scripts and tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Premissa&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sensor_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="no"&gt;Premissa&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;:temp_high&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sensor_temp&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;mem&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sensor_temp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;criar_fato_derivado:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sensor_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Premissa updates :temp_high to true; subscribers to :temp_high get notified.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start &lt;strong&gt;source&lt;/strong&gt; facts before premises and rules that depend on them; order matters because &lt;code&gt;obter/1&lt;/code&gt; and initial premise evaluation run at &lt;code&gt;init&lt;/code&gt; time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runnable sketch in &lt;code&gt;iex&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;With the library as a dependency (or &lt;code&gt;path:&lt;/code&gt; in &lt;code&gt;mix.exs&lt;/code&gt;), after &lt;code&gt;Application.ensure_all_started(:tec0301_pon)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Regra&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:counter&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="n"&gt;condition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;mem&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_mem&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"counter reached 3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule_pid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Regra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Fato&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atualizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# When the condition evaluates to true after an update, the action runs.&lt;/span&gt;
&lt;span class="c1"&gt;# Use Regra.start_link(..., modulo, edge_triggered: true) for false→true transitions only (module API).&lt;/span&gt;

&lt;span class="no"&gt;Regra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;estatisticas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule_pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; %{notificacoes: ..., execucoes: ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;Process.sleep/1&lt;/code&gt; sparingly in tests; for deterministic waits, the optional &lt;code&gt;Service&lt;/code&gt; below can help drain work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: &lt;code&gt;Service&lt;/code&gt; for registration and global stats
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Tec0301Pon.PON.Service&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; started by &lt;code&gt;Application&lt;/code&gt;. If you start it yourself, you can &lt;strong&gt;register&lt;/strong&gt; fact names and rule pids, then query &lt;strong&gt;aggregated&lt;/strong&gt; counters (&lt;code&gt;estatisticas_globais/0&lt;/code&gt;) or call &lt;code&gt;wait_until_queues_empty/1&lt;/code&gt; in tests. It does not participate in the notification protocol itself—it is observability glue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_link&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registrar_fato&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;registrar_regra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:my_rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule_pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Tec0301Pon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;estatisticas_globais&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we defer to later parts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defrule&lt;/code&gt; / &lt;code&gt;defpremissa&lt;/code&gt; and generated modules&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;&lt;strong&gt;Part 3 on dev.to&lt;/strong&gt;&lt;/a&gt; (metaprogramming and less boilerplate).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ports &amp;amp; adapters&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/hexagonal-architecture-pon-ports-adapters-to-decouple-the-engine-3l54"&gt;&lt;strong&gt;Part 4 on dev.to&lt;/strong&gt;&lt;/a&gt; (keeping side effects behind boundaries).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message storms, coalescing, ETS tuning&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/when-notifications-explode-message-storms-deduplication-and-back-pressure-in-pon-34p4"&gt;&lt;strong&gt;Part 10 on dev.to&lt;/strong&gt;&lt;/a&gt;; &lt;a href="https://dev.to/matheuscamarques/dev-profiling-cpu-memory-and-what-changed-after-optimizations-28hb"&gt;&lt;strong&gt;Part 11 on dev.to&lt;/strong&gt;&lt;/a&gt; (profiling).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Facts are &lt;strong&gt;named GenServers&lt;/strong&gt;; the &lt;strong&gt;Registry&lt;/strong&gt; &lt;code&gt;Tec0301Pon.PON.PubSub&lt;/code&gt; is a &lt;strong&gt;duplicate-key&lt;/strong&gt; topic bus. On change, a fact &lt;strong&gt;dispatches&lt;/strong&gt; to all registered pids with &lt;code&gt;{:notificacao, name, value}&lt;/code&gt;. Rules and premises are &lt;strong&gt;subscriber processes&lt;/strong&gt; that register on fact keys and update local state or &lt;strong&gt;downstream&lt;/strong&gt; facts. That is the whole whiteboard-to-OTP bridge in this PoC.&lt;/p&gt;

&lt;h2&gt;
  
  
  References and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elixir &lt;code&gt;Registry&lt;/code&gt;&lt;/strong&gt; — duplicate keys, dispatch patterns — &lt;a href="https://hexdocs.pm/elixir/Registry.html" rel="noopener noreferrer"&gt;HexDocs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elixir &lt;code&gt;GenServer&lt;/code&gt;&lt;/strong&gt; — cast/call, state — &lt;a href="https://hexdocs.pm/elixir/GenServer.html" rel="noopener noreferrer"&gt;HexDocs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Armstrong (2003)&lt;/strong&gt; — &lt;em&gt;Making reliable distributed systems…&lt;/em&gt; — &lt;a href="https://www.erlang.org/download/armstrong_thesis_2003.pdf" rel="noopener noreferrer"&gt;thesis PDF&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cesarini &amp;amp; Thompson&lt;/strong&gt; — &lt;em&gt;Programming Erlang&lt;/em&gt; (2nd ed.) — OTP supervision and process design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repo&lt;/strong&gt; — &lt;a href="https://dev.to/matheuscamarques/bibliography-pon-smart-brewery-devto-series-en-drafts-58a9"&gt;Bibliography on dev.to — PON + Smart Brewery series (EN drafts)&lt;/a&gt; · &lt;a href="//../BIBLIOGRAPHY_PON_SERIES.md"&gt;repo draft&lt;/a&gt;; &lt;strong&gt;Code map:&lt;/strong&gt; &lt;code&gt;lib/tec0301_pon/application.ex&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/pon/fato.ex&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/pon/regra.ex&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/pon/premissa.ex&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/pon/service.ex&lt;/code&gt;, &lt;code&gt;lib/tec0301_pon/pon/fanout.ex&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Published on dev.to:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/from-whiteboard-to-code-mapping-facts-rules-and-premises-to-otp-processes-1blb"&gt;From whiteboard to code: mapping Facts, Rules, and Premises to OTP processes&lt;/a&gt; — tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/notification-oriented-paradigm-pon-in-elixir-why-the-beam-fits-reactive-rules-2p9e"&gt;Part 1 on dev.to — PON in Elixir: why the BEAM fits reactive rules&lt;/a&gt; · &lt;a href="//01_pon_in_elixir_why_beam.md"&gt;repo draft&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://dev.to/matheuscamarques/a-metaprogrammed-dsl-defrule-and-defpremissa-with-less-pon-boilerplate-3909"&gt;Part 3 on dev.to — A metaprogrammed DSL: &lt;code&gt;defrule&lt;/code&gt; and &lt;code&gt;defpremissa&lt;/code&gt; with less PON boilerplate&lt;/a&gt; · &lt;a href="//03_metaprogrammed_dsl_defrule_defpremissa.md"&gt;repo draft&lt;/a&gt;. Author hub: &lt;a href="https://dev.to/matheuscamarques"&gt;dev.to/matheuscamarques&lt;/a&gt;; URLs tracked in &lt;a href="//../../devto_serie_pon_smart_brewery.md"&gt;&lt;code&gt;docs/devto_serie_pon_smart_brewery.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>otp</category>
      <category>architecture</category>
      <category>registry</category>
    </item>
  </channel>
</rss>
