Skip to content

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.

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.

LevelMeaningClass
CategoryOne engine execution = one input file. ~20 categories (baseline, improve, tax, estate…).Input/Categories/Category.php
RunOne projection scenario within a category. A category has 1+ runs; run 0 is the base run.Input/Run.php
SectionA labeled block of the input (Family Info, Tax, Insurance…). ~46 sections.Input/Sections/Section.php
LineEither a header string (!!! … !!!, --- … ---, === … ===) or a tuple [type, key, [values]].

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 pointHousehold::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()).

This is a multi-level template-method + factory composition, not a single orchestrator.

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):

GroupCategories
Corebaseline, baseline_scenario, improve, improve_scenario
Cash flowcash_flow_scenario
Debtdebt, student_loan
Insurancedisability_insurance, life_insurance, ltc_insurance
Educationeducation
Estateestate
Businessbusiness, business_strategies
Social Securitysocial_security
Stock planstock_plan
Taxtax_analyzer, tax_efficient, tax_strategy_optimization
Risksensitivity_stochastic
Uploaded tax returnsuploaded_return_2023uploaded_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())]],
];

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 ChildrenChildrenEngineModel (EngineModels/FamilyInfo/ChildrenEngineModel.php:24, which maps App\Models\Person fields).

Master ordered list: Run::SECTIONS_BY_NAME at app/Engine/Calc/Input/Run.php:88-136. Grouped by domain:

DomainSection classes
Run controlModelSection, ProjectionInfoSection
HouseholdFamilyInfoSection
Income / ExpenseIncomeAssumptionsSection, ExpenseAssumptionsSection
DebtExtraDebtPaymentSection, LoanSection
TaxTaxSection + TaxOverridesGeneralInputsSection, TaxOverridesForm1040Section, TaxOverridesScheduleA/D/1/1A/2/SeSection, TaxOverridesForm6251Section
Savings / InvestmentsSavingStrategySection, InvestedAssetSection, AccountLevelAssumptionsSection, AssetAllocationReturnAndVolatilityAssumptionsSection
EducationCollege529AndInfoSection
Retirement accountsInheritedIraSection, AnnuitySection, WithdrawalSection
Real estateHomeSection, HomeModificationSection, ReverseMortgageSection, RentalScheduleSection
BusinessBusinessesSection, BusinessScheduleSection
Other assetsOtherAssetsSection, OptionsSection
InsuranceInsuranceSection, AdditionalLifeInsuranceSection, LifeInsuranceScheduleSection
Estate / TrustEstateSection, TrustSection
Social SecuritySocialSecuritySection
GoalsFinancialGoalsSection
HealthHealthCostsSection
AssumptionsInflationRatesSection, MiscItemsSection
Spending strategyDynamicRetirementSpendingStrategySection, RetirementSpendingReductionOverrideSection
Overrides / ScenarioOverrideSection, ScenarioSection

Section files live in app/Engine/Calc/Input/Sections/ (54 files).

  • 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/Input take an alternative_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 during generate().
  • Plan settings / assumptions: e.g. household_assumption->effective_misc->child_leaves_home; valuation date from HouseholdExtra::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.

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):

TypeMeaningEncoding
Bbool1 / 0
Ddateas string
Iintdecimal
Nnumberdecimal (scientific notation expanded)
Sstringwrapped {…}, joined },{

Format version: 6.9.64 (Category.php:205-208).

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):

  1. Subprocess (runViaSubprocess): writes input to a temp file, runs the CalcEngine binary via Symfony Process with INPUT_FILE / SCENARIO_FILE / OUTPUT_FILE / ERROR_FILE / DUMMY_FILE, reads the output file. Timeout 235s.
  2. engine-api HTTP (runViaEngineApi) → App\Services\EngineApi\Client::calculate() (app/Services/EngineApi/Client.php:42-93): POST {base_url}/engine_version/{version}/calculate with 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).

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}/ as input.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() calls EngineRunner::run() and stores output.

Output is parsed by app/Engine/Calc/Output/Reader.php + Parser.php, returned via Calculator::getOutputReader().

  • 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 own Calculator + Input/Categories) for uploaded tax-return analysis. These are the uploaded_return_* categories.
ConcernFile:line
Public entry (factory)app/Models/Household.php:312-326
Façade + hierarchy doc + type constsapp/Engine/Calc/Calculator.php:23-41, :97-122
Per-category lazy factory / origin runapp/Engine/Calc/Input/Input.php:44-199
Category names enumapp/Engine/Calc/Input/CategoryName.php
Category orchestrator (header, toArray, generate)app/Engine/Calc/Input/Categories/Category.php:148-296
Concrete category exampleapp/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 sectionsapp/Engine/Calc/Input/Sections/SectionWithSubsectionObjects.php
Concrete section exampleapp/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 clientapp/Services/EngineApi/Client.php:42-93
Cache/S3 + execution orchestrationapp/Engine/Calc/Support/HouseholdCalculation.php
Runner configconfig/services.php:156-167
Output readingapp/Engine/Calc/Output/Reader.php, Parser.php
Debug: dump raw input per categoryapp/Http/Controllers/Internal/CalcEngineAnalysisPlatform/HouseholdController.php:48-98
  • retail-api (web-service/api) repo, app/Engine/Calc/ — read at commit on branch develop, 2026-06.
  • Class docblock app/Engine/Calc/Calculator.php:23-41 (canonical hierarchy description).