← jason fetzerShift Investigatordocs
Back to the investigatorGolden evalsSynthetic data / read-only tools

Synthetic record families

src/investigator/records.py defines four plain dataclasses whose field names and units mirror the shape of a corrugator controller's own record types: the dry-end setup record ("P04P"-shaped), the wet-end record ("P04G"-shaped), the shift record ("P04T"-shaped) and the stop record ("P04S"-shaped). They are our own documented demo schema (demo-records/v1). The names are borrowed so the data reads like plant records; nothing here claims OEM protocol or wire-format compatibility, and no vendor field semantics are asserted beyond what this page states.

src/investigator/synth.py generates any range of production days of one machine from a seed, one day at a time: every random draw of a day comes from streams keyed by the seed and a date (issue #35), so a day is byte-identical in every range that contains it. The app's data source is a rolling year: every production day from 365 days before as_of to as_of (INVESTIGATOR_AS_OF; the image's build date minus one day; 2026-09-30 for the tests and the golden set). src/investigator/record_store.py builds that window once per process (or loads it prebuilt) into one in-memory DuckDB, derives the per-day tables the daily tools read (RecordStore.day_analysis, see AGENT_EXECUTION) and answers the period tools over the whole window (METRICS). The one-day XML fixture (fixture.generate()) remains only for its own tests.

Generating

from datetime import date
from investigator.synth import generate_day, generate_months, generate_window, write_jsonl, read_jsonl
data = generate_window(date(2026, 9, 30), seed=2026, machine_id="CORR-1")   # the year ending 2026-09-30
data = generate_months(seed=2026, months=(("2026", 8), ("2026", 9)))          # or any months
day = generate_day(date(2026, 9, 14))     # one production day: the same records as inside either range
data["content_hash"]            # sha256 over the four JSONL files of the range
write_jsonl(data, "artifacts/synthetic")   # four .jsonl files + manifest.json
same = read_jsonl("artifacts/synthetic")   # dataclasses again; manifest hash must match

The same seed gives byte-identical JSONL, and a day gives the same records whatever range it is generated in. Two months generate in about a second, the year in about six. Generated files are ignored by git (*.jsonl); write them under artifacts/.

Units and conventions

  • Feet, inches, square feet, ft/min, whole seconds. Dates YYYYMMDD, times HHMMSS, durations HHMMSS, all in the fixed demo-local clock (no timezone, no DST).
  • Production days are Monday to Friday. Weekends are plant-down and emit no records; there is no holiday calendar. A production day starts 07:00 and is labelled by its start date; shift 1 = 07:00, 2 = 15:00, 3 = 23:00, each 480 scheduled minutes.
  • lineal_ft counts every foot of web through the machine: order sheets and full-web shear chops. Gross area is therefore exactly lineal_ft x web_width_in / 12. Order travel (what reached the cutoff knives) is lineal_ft - chop_sqft x 12 / web_width_in.
  • Knives share travel: nominal cuts on each knife multiply out to the same order travel within one cutoff length. Outs multiply sheets and occupied width, never distance.
  • Web geometry: sum(sheet_width_in x number_of_outs) + trim_in = web_width_in, trim split evenly (1-7 inches per side), web centered. Web widths 87-106 in on a 110-inch machine (106, 103, 100, 98, 95, 92, 87, weighted to a 98-inch average). Grades and target speeds are fixture.GRADES.
  • Averages include downtime: a setup's avg_speed_fpm is lineal over elapsed minutes with stops inside; a shift's is total lineal over 480 minutes. max_speed_fpm on wet ends and shifts is the highest running speed (lineal over running minutes) of their setups.
  • Rounding: feet, square feet and speeds carry one decimal, percentages two. Aggregates are sums of the rounded setup values, rounded once more.

DryEndSetupRecord (one per dry-end setup)

FieldUnitMeaning
setup_idtext20 characters: 4-char machine code + start YYYYMMDDHHMMSS + 2-digit sequence in the shift
machine_idtextthe corrugator (CORR-1)
start_date, start_time, end_date, end_timeYYYYMMDD / HHMMSSrecord interval; end = start + running + stop seconds
lineal_ftftall web travel, order sheets and chops
avg_speed_fpmft/minlineal_ft / elapsed minutes, stops included
web_width_ininpaper width, one of 106, 103, 100, 98, 95, 92, 87 (98-inch average)
quality_idtextboard grade key of fixture.GRADES
total_nominal_ftftplanned order travel of the guide customer: nominal_cuts x sheet_length_in / 12 (knives share travel; not a sum over customers)
total_produced_sqftsq ftgross area, trim and chops included
trim_sqftsq ftside trim = order travel x trim_in / 12
trim_pct%100 x trim_sqft / total_produced_sqft
change_codeO / S / Cwhy this record started: O the knife order combination changed; S shift split, continuing split_from_setup_id across a shift boundary; C continuation, knife 1's order continued while another knife's order changed
stop_time_sec, number_of_stopss, countthe stops that fell inside the record (equal to the StopRecords naming this setup_id)
shift_number1-3shift the record lies in
chop_sqftsq ftfull-web shear chops, 34 inches each, part of gross
kqf_sqftsq ftdemo definition: total dry-end reject area (sum of rejects)
customerslist1-3 CustomerBlock, one order per knife position
production_dateYYYYMMDDproduction day (07:00 start) of the shift
rejectslistnon-protocol RejectEntry lines
wet_end_idtextthe wet-end record that aggregates this setup
split_from_setup_idtext / nullfor S, the record this one continues

CustomerBlock

FieldUnitMeaning
order_numbertextSO-YYMMDDnnnn: the production day and the order's sequence in it; unique per order, once per setup, may continue onto the next setup inside the day
part_numbertextP-nnnnn
sheet_width_in, sheet_length_ininordered sheet; length is the cutoff (20-92)
number_of_outscountsheets across the web for this order
cutoff_allocation1-3knife position = position index + 1
nominal_cutscountplanned cuts for this record; nominal_cuts x sheet_length_in / 12 is within one cutoff of the order travel
actual_cutscountcounted cuts; within +-10% of nominal, most within +-2% (the over/under-run window)
hqm_waste_sqftsq ftreject area attributed at the cutoff knife (misalignment)
asm_waste_sqftsq ftreject area attributed at the stacker (bond, warp)
stack_height_sheetscountsheets per stack

RejectEntry (non-protocol)

FieldUnitMeaning
reasonBOND / WARP / MISALIGNMENT / MOISTUREreported family, not a proven cause
detailtextreported sub-reason within the family: warp is Up warp / Down warp / Twist warp / S-warp, bond is Delamination / Paper / raw material; misalignment and moisture carry their own name (owner taxonomy 2026-10-22)
cutoff_allocation1-3whose sheets were rejected
sheetscountindividual sheets; never multiplied by outs again
sqftsq ftsheets x sheet_width_in x sheet_length_in / 144

WetEndRecord (one per paper run)

FieldUnitMeaning
wet_end_idtextWE + production date + 3-digit sequence
quality_id, web_width_inconstant across its setups
start_date, start_time, end_date, end_time, durationfirst setup start to last setup end; duration HHMMSS
lineal_ft, total_produced_sqft, side_trim_sqftft, sq ftexact sums of its setups
max_speed_fpmft/minhighest running speed of its setups
avg_speed_fpmft/minlineal_ft / elapsed minutes, stops included
setup_idslistsetups aggregated, in time order and contiguous
shift_numberslistshifts spanned (one or two); a run that crosses a boundary holds an S setup or a setup ending exactly on it
production_dateYYYYMMDDproduction day of its first setup

ShiftRecord (one per 480-minute shift)

FieldUnitMeaning
shift_number, production_date1 / 2 / 3 within the production day
group_nametextcrew label Crew A / Crew B / Crew C, rotating weekly; never a person's name
start_date, start_time, end_date, end_time, durationfixed window; duration 080000
total_lineal_ft, total_produced_sqft, side_trim_sqft, chop_sqft, reject_sqftft, sq ftexact sums of the setups carrying this date and shift
max_speed_fpmft/minhighest running speed of its setups
avg_speed_fpmft/mintotal_lineal_ft / 480, downtime included
wet_end_idslistwet ends with at least one setup in the shift
scheduled_minutesmin480
bale_sqftsq ftnon-protocol scale reading of baled waste = trim + chops + rejects + an unexplained remainder of 0.1-0.4% of gross, so bale percent reads slightly above the sum of its parts

StopRecord (one per machine stop)

FieldUnitMeaning
stop_idtextST + start YYYYMMDDHHMMSS
start_date, start_time, end_date, end_time, duration_secwhole seconds; stops never overlap and always sit inside one setup
kindMaintenance / Operator / PMPM = planned maintenance window; every PM stop carries the reason Planned maintenance. A Power outage stop is recorded as Maintenance; the investigator's tables count it as External (the owner's rule in targets.json, PR #111)
placetextUpper knife, Lower knife, Shear knife, Slitter/scorer, Double backer, Splicer 1-5, Glue dams, Stacker, Single facer, Steam system, Power
reasontextnine unplanned reasons - Knife / slitter jam, Missed splice, Raw material (roll), Stacker, Order change, Glue / starch, Steam / moisture, Power outage, Other - plus Planned maintenance, used only by PM-kind stops so the planned window is its own Pareto row rather than inflating Other
shift_number, production_datethe shift the stop fell in
setup_idtextthe setup record that contains it
user_infotextsynthetic operator note, always labelled unverified

Generator behaviour and the bands it keeps

These are simulation choices (synth.py constants), not plant standards. tests/test_records_invariants.py checks each line for every record of August and September 2026.

  • Seeding by date (issue #35): a production day draws from Random(f"{seed}:{day}"); its week's speed factor from Random(f"{seed}:week:{monday}"); its month's power outage from Random(f"{seed}:outage:{yyyy-mm}"), chosen among every shift of the whole month; crews rotate weekly counted from Monday 2026-08-03. Nothing carries from one production day into the next, so a day regenerates byte-identically in any range, and moving the window by a day changes no day it keeps (tests/test_synth_determinism.py).
  • Time: setups tile each shift with no gaps; a setup that would cross the boundary between shifts 1 and 2 or 2 and 3 is split there (first part keeps its code, second part is S and shares the orders). A remainder with under 60 s of running time is absorbed; a first part under 60 s stretches the prior record instead. At the end of shift 3 (07:00 the next morning) the open run ends and the next production day starts fresh, as after a weekend, so no record or paper run crosses a production day.
  • Output: about 2.5 million gross sq ft per shift (6-8.5 million per production day) at a 98-inch average web on a 110-inch machine; about 7,000 lineal ft per dry-end setup (roughly 100-140 setups a day) and about 20,000 lineal ft per wet-end run (roughly 40 paper changes a day); week-to-week volume moves a few percent through a per-week speed draw. Running speed is the grade target x crew factor (0.88-0.94) x shift factor x noise; displayed averages include stops.
  • Downtime: Maintenance + PM between 2% and 3% of scheduled minutes per month; PM is one 120-135 minute window every Thursday on shift 1 (the rebuild week runs 180 min), recorded with kind PM and reason Planned maintenance, plus unplanned maintenance stops (Monday x1.40, August x1.08; operator stops Monday x1.15), one 10-15 minute power outage per month. Operator stops separately 1-3%.
  • Waste per month: side trim 2.5-3.5% of gross, shear chops 0.4-1.0%, dry-end rejects about 1.0-1.1% split bond 0.40 / warp 0.30-0.42 / misalignment 0.20 / moisture 0.10 points; inside warp about 45% up, 25% down, 20% twist, 10% S; inside bond 60% delamination, 40% paper / raw material. Rejected sheets are discarded; they are never replaced in the records. Each missed splice adds a run of 8-14 chops (a web break).
  • Determinism: the content hash of August and September 2026 is pinned in tests/test_synth_determinism.py, and that of the golden year in evals/golden_values.json; change them only with an intended generator change.

Seeded stories

The answer keys live in src/investigator/stories.py and are read only by the generator; no tool, analysis, agent, server or report module imports them (pinned by test). The records must reveal them on their own:

  1. A slitter/scorer rebuild in the Thursday 20 August PM window; afterwards side trim creeps about 0.1 percentage point per week through 31 October 2026, then returns to its pre-story level, and knife / slitter jam stops rise, concentrating on the slitter/scorer.
  2. A missed-splice problem on Splicer 3 in the week of 7-13 September: missed-splice minutes roughly triple, stop counts rise, shear chops follow.
  3. Warp rejects are higher on the first production day after a weekend (Monday x1.5) and in August (0.42% of gross) than in September (0.30%) or any other month.

The stories keep their 2026 dates in every window. The first's trim creep ends on 31 October 2026 (owner, PR #91): side trim of gross runs about 2.8% before the rebuild, 3.2% in September and 3.6% in October, and is back at about 2.8% from November (trim_target_pct in synth.py, pinned by test_trim_creep_ends_on_31_october_and_trim_returns_to_its_pre_story_level). The knife / slitter jam stops keep no end date: they stay three times the baseline in every window after the rebuild.

Not modelled

Holidays, timezone / DST, overlapping or shift-crossing stops, an order quantity ledger (requested / remaining sheets), reject replacement, accepted-shipment accounting, paper recipes, and any vendor field semantics beyond the definitions above.

Rendered from docs/RECORDS.md in the repository.