Engine Input Construction
The calculation engine (“Engine” / “CalcEngine”) is a separate compiled binary. retail-api builds a plain-text, line-based input payload per “category”, then either runs the binary as a subprocess or sends the text to the engine-api HTTP service. All of this lives under app/Engine/Calc/ in the retail-api (web-service/api) repo.
Mental Model
Section titled “Mental Model”Household.getCalculator() → Calculator (façade, holds the Input factory) → Input (factory: builds one DB-reading origin Run, clones per category) → Category (one engine execution = one input file; ~20 categories) → Run (one projection scenario; run 0 = base run) → Section (~46 labeled blocks: Family Info, Tax, Insurance…) → Line (header string, or [type, key, [values]] tuple) → EngineModel (middle layer over Eloquent models + TweakSets, for repeating records)One-line flow: Household.getCalculator() returns a Calculator holding an Input factory. Input builds a DB-reading origin Run once, then clones it per Category. Each category’s generate() clones and tweaks Runs. Each Run concatenates ~46 Sections. Sections render Lines (directly or via EngineModels). CategoryHelper flattens lines to text. HouseholdCalculation hashes/caches it, and EngineRunner ships it to the engine.
The canonical description of this hierarchy is in the class docblock at app/Engine/Calc/Calculator.php:23-41.
The Four-Level Hierarchy
Section titled “The Four-Level Hierarchy”| Level | Meaning | Class |
|---|---|---|
| Category | One engine execution = one input file. ~20 categories (baseline, improve, tax, estate…). | Input/Categories/Category.php |
| Run | One projection scenario within a category. A category has 1+ runs; run 0 is the base run. | Input/Run.php |
| Section | A labeled block of the input (Family Info, Tax, Insurance…). ~46 sections. | Input/Sections/Section.php |
| Line | Either a header string (!!! … !!!, --- … ---, === … ===) or a tuple [type, key, [values]]. | — |
Entry Points
Section titled “Entry Points”There is no class literally named EngineInputBuilder. The builder role is split across Input (factory), Category (orchestrator), Run (section assembler), and Section subclasses.
Public entry point — Household::getCalculator() at app/Models/Household.php:312-326:
public function getCalculator(TweakSet $alternative_tweak_set, TweakSet $benchmark_tweak_set): Calculator{ // ...cached per (alternative, benchmark) tweak-set key $this->calculators[$key] = new Calculator($this, $alternative_tweak_set, $benchmark_tweak_set); return $this->calculators[$key];}Calculator (app/Engine/Calc/Calculator.php:42) is the façade. Its constructor (:97-111) creates the Input factory. getOutputReader(CategoryName) (:113-122) triggers the full input → engine → output cycle:
// Access an output$calculator->getOutputReader(CategoryName::BASELINE)->getValues('run_0.probability');
// Access an input (for debugging)$calculator->input->baseline->getRun(0)->family_info->get('Client|Person ID');Controllers call getCalculator(...)->getOutputReader(CategoryName::X) — roughly 40 call sites, e.g. app/Http/Controllers/Calculate/Advisors/Households/Retirement/AnalysisController.php:58-59, .../Tax/AnalysisController.php:68, .../Estate/AnalysisController.php:106.
Debug / inspection — dump the raw input text per category at app/Http/Controllers/Internal/CalcEngineAnalysisPlatform/HouseholdController.php:48-98 (key line :86: $calculator->input->{$category_name->value}->toString()).
Builder Composition
Section titled “Builder Composition”This is a multi-level template-method + factory composition, not a single orchestrator.
1. Input — per-category lazy factory
Section titled “1. Input — per-category lazy factory”app/Engine/Calc/Input/Input.php:44. The closest thing to an “input builder”. It declares one lazy property per category. Each category is created via createCategory() (:164-172) from a single shared origin run:
private function createCategory(CategoryName $name, string $fqcn, TweakSet $base_tweak_set): Category{ $category = new $fqcn($name, $this->household, $this->valuation_date, $base_tweak_set); $category->assignBaseRun(clone $this->getOriginRun()); // deep clone, no re-read of DB return $category;}The origin run (getOriginRun() :179-199) is the only run that reads data from the DB. It is built once, preBuild() eager-loads everything, and every category clones it. This is why “the first category is very slow” and the rest are cheap.
Note which categories ignore the alternative/benchmark tweak sets and always use the baseline tweak set: debt, disability_insurance, life_insurance (see Input.php:67-95).
2. Category — orchestrator (~20 subclasses)
Section titled “2. Category — orchestrator (~20 subclasses)”app/Engine/Calc/Input/Categories/Category.php:30 (abstract). Holds runs[], defines abstract generate() (:262), and toArray() (:148-168) which concatenates the header + all runs.
The category names live in the CategoryName enum (app/Engine/Calc/Input/CategoryName.php):
| Group | Categories |
|---|---|
| Core | baseline, baseline_scenario, improve, improve_scenario |
| Cash flow | cash_flow_scenario |
| Debt | debt, student_loan |
| Insurance | disability_insurance, life_insurance, ltc_insurance |
| Education | education |
| Estate | estate |
| Business | business, business_strategies |
| Social Security | social_security |
| Stock plan | stock_plan |
| Tax | tax_analyzer, tax_efficient, tax_strategy_optimization |
| Risk | sensitivity_stochastic |
| Uploaded tax returns | uploaded_return_2023 … uploaded_return_2025_to_2026 (separate pipeline) |
Each subclass implements generate() to clone the base run and apply domain-specific tweaks. Example — BaselineCategory::generate() at app/Engine/Calc/Input/Categories/BaselineCategory.php:20-35.
The category header (getHeader() Category.php:269-278) emits format version, valuation date, and run count:
[ '!!! Header !!!', [Calculator::VAR_TYPE_INT, 'Run type', [0]], [Calculator::VAR_TYPE_STRING, 'Version', [self::getFormatVersion()]], // '6.9.64' [Calculator::VAR_TYPE_DATE, 'Valuation date', [$this->valuation_date->toDateString()]], [Calculator::VAR_TYPE_INT, 'Number of runs', [count($this->getRuns())]],];3. Run — section assembler
Section titled “3. Run — section assembler”app/Engine/Calc/Input/Run.php:85. Instantiates all sections from the master ordered registry Run::SECTIONS_BY_NAME (:88-136), deep-clones them on __clone(), and preBuild() eager-loads.
Run::toArray() (:286-298) wraps the sections with run header/footer:
$lines = [];foreach ($this->getSectionsBySnakeCaseName() as $section) { $lines = array_merge($lines, $section->toArray());}return array_merge([$this->getHeader()], $lines, [$this->getFooter()]);// header: '!!! Run N !!!' footer: '=== End of User Inputs! ==='4. Section — line generator (~46 sections)
Section titled “4. Section — line generator (~46 sections)”app/Engine/Calc/Input/Sections/Section.php:24 (abstract). Template method abstract generateLines() (:92); toArray() = header + lines (:74-78). Lines for the origin run (whose category is null) are cached in Redis under in.<section_id> (:59-65).
Section header format (getHeader() :116-128): --- <ID>. <Name> ---.
SectionWithSubsectionObjects (app/Engine/Calc/Input/Sections/SectionWithSubsectionObjects.php:23) handles repeating records (children, accounts, loans…). It drives EngineModels via SUBSECTION_DEFINITIONS and generateSubsectionFromObjects().
5. EngineModel — data middle layer (~27 model groups)
Section titled “5. EngineModel — data middle layer (~27 model groups)”app/Engine/Calc/Input/EngineModels/EngineModel.php:25 — “the middle layer of Section and data source (Eloquent Model and Tweak)”. Defines getSpecs() (type + label schema) and getValuesByLabel(). Source-backed variant: EngineModelWithSourceModel.
Concrete example chain: FamilyInfoSection (Sections/FamilyInfoSection.php:16) → subsection Children → ChildrenEngineModel (EngineModels/FamilyInfo/ChildrenEngineModel.php:24, which maps App\Models\Person fields).
Section Registry (the ~46 sections)
Section titled “Section Registry (the ~46 sections)”Master ordered list: Run::SECTIONS_BY_NAME at app/Engine/Calc/Input/Run.php:88-136. Grouped by domain:
| Domain | Section classes |
|---|---|
| Run control | ModelSection, ProjectionInfoSection |
| Household | FamilyInfoSection |
| Income / Expense | IncomeAssumptionsSection, ExpenseAssumptionsSection |
| Debt | ExtraDebtPaymentSection, LoanSection |
| Tax | TaxSection + TaxOverridesGeneralInputsSection, TaxOverridesForm1040Section, TaxOverridesScheduleA/D/1/1A/2/SeSection, TaxOverridesForm6251Section |
| Savings / Investments | SavingStrategySection, InvestedAssetSection, AccountLevelAssumptionsSection, AssetAllocationReturnAndVolatilityAssumptionsSection |
| Education | College529AndInfoSection |
| Retirement accounts | InheritedIraSection, AnnuitySection, WithdrawalSection |
| Real estate | HomeSection, HomeModificationSection, ReverseMortgageSection, RentalScheduleSection |
| Business | BusinessesSection, BusinessScheduleSection |
| Other assets | OtherAssetsSection, OptionsSection |
| Insurance | InsuranceSection, AdditionalLifeInsuranceSection, LifeInsuranceScheduleSection |
| Estate / Trust | EstateSection, TrustSection |
| Social Security | SocialSecuritySection |
| Goals | FinancialGoalsSection |
| Health | HealthCostsSection |
| Assumptions | InflationRatesSection, MiscItemsSection |
| Spending strategy | DynamicRetirementSpendingStrategySection, RetirementSpendingReductionOverrideSection |
| Overrides / Scenario | OverrideSection, ScenarioSection |
Section files live in app/Engine/Calc/Input/Sections/ (54 files).
Data Sources
Section titled “Data Sources”- Primary:
App\Models\Household(Eloquent) + related models —Person,AdultPerson,Income,Asset,RelocationPlan,HouseholdExtra, etc. - TweakSets (
App\Models\TweakSet): the proposed/alternative vs. benchmark scenario deltas.Calculator/Inputtake analternative_tweak_set+benchmark_tweak_set. Categories resolve additional system tweak sets (debt, education, estate, life/disability insurance, stock plan, stress test, student loan) and apply them duringgenerate(). - Plan settings / assumptions: e.g.
household_assumption->effective_misc->child_leaves_home; valuation date fromHouseholdExtra::getValuationDate()(Calculator.php:100). - Caching: origin-run section data is cached in Redis keyed
in.<section_id>with engine cache tags (Section.php:60). Calculation results are cached by content hash.
Output Format & Transmission
Section titled “Output Format & Transmission”Text serialization
Section titled “Text serialization”The input is plain text, one line per record, \n-joined. Category::toString() → CategoryHelper::convertLinesToString() (app/Engine/Calc/Support/CategoryHelper.php:26-65).
Line encoding: {TYPE|Label},val1,val2. Value types (Calculator.php:47-59):
| Type | Meaning | Encoding |
|---|---|---|
B | bool | 1 / 0 |
D | date | as string |
I | int | decimal |
N | number | decimal (scientific notation expanded) |
S | string | wrapped {…}, joined },{ |
Format version: 6.9.64 (Category.php:205-208).
Two runners
Section titled “Two runners”Selected by config('services.engine.runner') (config/services.php:156-167, env ENGINE_RUNNER, default subprocess). Dispatch: EngineRunner::run() (app/Engine/Calc/Support/EngineRunner.php:98-122):
- Subprocess (
runViaSubprocess): writes input to a temp file, runs the CalcEngine binary via SymfonyProcesswithINPUT_FILE/SCENARIO_FILE/OUTPUT_FILE/ERROR_FILE/DUMMY_FILE, reads the output file. Timeout 235s. - engine-api HTTP (
runViaEngineApi) →App\Services\EngineApi\Client::calculate()(app/Services/EngineApi/Client.php:42-93):POST {base_url}/engine_version/{version}/calculatewith JSON body:
{ "engine_input_content": "...", "engine_input_format_version": "6.9.64", "global_scenario_identifier": "...", "custom_scenario_input_content": "...", "multi_threading": "auto"}HTTP timeout is 240s (5s on top of the binary timeout for transport).
Orchestration, hashing & S3 caching
Section titled “Orchestration, hashing & S3 caching”HouseholdCalculation (app/Engine/Calc/Support/HouseholdCalculation.php):
- Builds input via
input_category->toString(). - Computes
calculation_id = sha1(engine_version + input + scenario). - Caches input/output to S3 (gzipped) under
engine/calculations/{engine_version}/{household_id}/{calculation_id}/asinput.txt.gz,output.txt.gz,error.txt.gz,source-map.json.gz. The calculation id is cached in Redis. - Emits a source map (
Category::toSourceMap()Category.php:249-257,Run::toSourceMap()) mapping engine input IDs back to household models, uploaded for the “Iris” traceability tool. executeEngine()callsEngineRunner::run()and stores output.
Output is parsed by app/Engine/Calc/Output/Reader.php + Parser.php, returned via Calculator::getOutputReader().
Adjacent Pieces
Section titled “Adjacent Pieces”app/Engine/ScenGen/—ScenarioGenerator(+ Global/Household/Advisor/Organization variants) supplies the binary scenario file / scenario reference. See Asset Return & Correlation Assumptions for the 14-category/8-class data it consumes.app/Engine/Calc/UploadedTaxReturn/— a parallel mini-pipeline (its ownCalculator+Input/Categories) for uploaded tax-return analysis. These are theuploaded_return_*categories.
Key File Reference
Section titled “Key File Reference”| Concern | File:line |
|---|---|
| Public entry (factory) | app/Models/Household.php:312-326 |
| Façade + hierarchy doc + type consts | app/Engine/Calc/Calculator.php:23-41, :97-122 |
| Per-category lazy factory / origin run | app/Engine/Calc/Input/Input.php:44-199 |
| Category names enum | app/Engine/Calc/Input/CategoryName.php |
| Category orchestrator (header, toArray, generate) | app/Engine/Calc/Input/Categories/Category.php:148-296 |
| Concrete category example | app/Engine/Calc/Input/Categories/BaselineCategory.php:20-35 |
| Section registry (master ordered list) | app/Engine/Calc/Input/Run.php:88-136 |
| Run assembly (toArray, header/footer, clone) | app/Engine/Calc/Input/Run.php:286-298, :483-490 |
| Section base (template method, Redis cache) | app/Engine/Calc/Input/Sections/Section.php:24-128 |
| Repeating-record sections | app/Engine/Calc/Input/Sections/SectionWithSubsectionObjects.php |
| Concrete section example | app/Engine/Calc/Input/Sections/FamilyInfoSection.php |
| EngineModel (data middle layer) | app/Engine/Calc/Input/EngineModels/EngineModel.php:25 |
| Text serializer (line → string) | app/Engine/Calc/Support/CategoryHelper.php:26-65 |
| Runner dispatch (subprocess vs HTTP) | app/Engine/Calc/Support/EngineRunner.php:98-122 |
| HTTP engine-api client | app/Services/EngineApi/Client.php:42-93 |
| Cache/S3 + execution orchestration | app/Engine/Calc/Support/HouseholdCalculation.php |
| Runner config | config/services.php:156-167 |
| Output reading | app/Engine/Calc/Output/Reader.php, Parser.php |
| Debug: dump raw input per category | app/Http/Controllers/Internal/CalcEngineAnalysisPlatform/HouseholdController.php:48-98 |
Sources
Section titled “Sources”- retail-api (
web-service/api) repo,app/Engine/Calc/— read at commit on branchdevelop, 2026-06. - Class docblock
app/Engine/Calc/Calculator.php:23-41(canonical hierarchy description).