# dpm-synth dpm-synth generates synthetic EBA xBRL-CSV report packages that satisfy a module's DPM-XL validation rules. Give it a template that lists a module's tables, cells and rules; it fills every cell with a plausible value, repairs the values until the rules pass, and writes a report package that reads and validates like a real submission. dpm-synth is an independent project, not affiliated with or endorsed by the European Banking Authority. It builds on two sibling packages: [dpm-xl] evaluates the rules and [dpm-xbrl] writes the package. It needs Python 3.11 or newer. The reports it writes are synthetic: the entity is a fictional LEI and every value is generated. ## Who it is for - Teams that build loaders, data warehouses or validators for EBA reports and need realistic, internally consistent test submissions for any module, without using real supervisory data. - Performance tests that need report packages with millions of facts. - Rule authors who want to see a module's rules satisfied, or find the rules that cannot be satisfied together. ## In 30 seconds ```python from dpmxbrl import write_xbrl_csv from dpmsynth import generate, load_template template = load_template("minimal.json") # a module's tables, cells and rules result = generate(template, seed=7) print(result.passed, "of", result.rules, "rules pass; failing:", result.failed) write_xbrl_csv(result.report, "report.zip") for fact in result.report.facts: print(fact.template, fact.datapoint, fact.value) ``` ```text 2 of 2 rules pass; failing: [] t_01.00 dp1001 1126000 t_01.00 dp1002 971000 t_01.00 dp1003 155000 ``` The template [`minimal.json`](samples/minimal.json) has one table of three monetary cells and two rules: row 0010 is the sum of rows 0020 and 0030, and both components are not negative. The same from the command line: ```console $ dpm-synth generate minimal.json -o report.zip --seed 7 rules: 2, passed: 2, failed: 0, not evaluated: 0, not applicable: 0 rounds: 2 (failing rules per round: 1, 0) wrote report.zip (3 facts) ``` Every example in these pages runs against the templates in [`docs/samples`](https://github.com/luukhopman/dpm-synth/tree/main/docs/samples), and the test suite checks that each prints what is shown. ## Install ```text pip install dpm-synth ``` This installs dpm-xl and dpm-xbrl too, and the `dpm-synth` command. ## Key features - **Any module.** The template format describes tables, open keys, cells, data types, allowed items and rules, so one tool covers COREP, FINREP and every other DPM framework; see the [template format](reference/template-format.md). - **Rules satisfied, not just shapes.** Sums, differences, products of two cells, sign rules, `isnull`, `in` and `match` are repaired; on EBA DPM 4.3 COREP_OF, 999 of the 1,051 rules that apply pass. See [the evaluate-and-repair loop](concepts/repair-loop.md) and [its limits](concepts/limits.md). - **Valid packages.** Packages are written by dpm-xbrl and read back without a single broken EBA filing rule that the file can show, other than the synthetic file name. - **Reproducible.** The same template, seed, options and versions give the same bytes; see [reproduce a report](guides/reproducible-seeds.md). - **Large.** Scaled packages stream open tables with millions of rows in seconds; see [load-test with scaled packages](guides/load-testing.md). ## Toolkit **Part of the dpm toolkit**, three Python packages that work together: | Package | What it does | | :-- | :-- | | dpm-xl | Parse and evaluate DPM-XL validation rules | | dpm-xbrl | Read, write and check xBRL-CSV and XBRL-XML reports | | dpm-synth | Generate test reports that satisfy DPM-XL rules **(this package)** | The [toolkit tour](https://luukhopman.github.io/dpm-xl/guides/toolkit-tour/) takes one report through all three: generated with dpm-synth, read with dpm-xbrl, then evaluated and explained with dpm-xl. ## Where next - New here: the [getting started](getting-started.md) tutorial. - A task to do: the [how-to guides](guides/index.md). - How it works: the [concepts](concepts/index.md). - Exact behaviour: the [reference](reference/index.md), including the [template format](reference/template-format.md), the [API](reference/api.md) and the [command line](reference/cli.md). - Working with an AI agent: [dpm-synth for AI agents](agents.md), [`llms.txt`](llms.txt) and [`llms-full.txt`](llms-full.txt). - The sibling packages: [dpm-xl] for DPM-XL rules and [dpm-xbrl] for reading and writing reports. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Getting started This tutorial generates a synthetic report for a small template, reads the package back with dpm-xbrl and checks the rules with dpm-xl. It takes about ten minutes. ## Install ```text pip install dpm-synth ``` dpm-synth needs Python 3.11 or newer. It installs [dpm-xl], which evaluates DPM-XL rules, and [dpm-xbrl], which reads and writes EBA reports. ## The template A template describes one module: its tables, the cells of each table, and the rules a report must satisfy. It is a JSON document in the `dpm-synth-template/1` format. This is [`minimal.json`](samples/minimal.json): ```json { "format": "dpm-synth-template/1", "release": "1.0", "module": "DEMO", "entry_point": "https://example.com/taxonomy/demo/1.0/mod/demo.json", "reference_date": "2026-12-31", "tables": [ { "code": "T_01.00", "cells": [ {"id": "T_01.00/r0010", "row": "0010", "column": "0010", "datapoint": "dp1001", "data_type": "m"}, {"id": "T_01.00/r0020", "row": "0020", "column": "0010", "datapoint": "dp1002", "data_type": "m"}, {"id": "T_01.00/r0030", "row": "0030", "column": "0010", "datapoint": "dp1003", "data_type": "m"} ] } ], "rules": [ {"code": "v1", "expression": "with {tT_01.00, c0010, default: 0, interval: true}: {r0010} = {r0020} + {r0030}"}, {"code": "v2", "expression": "with {tT_01.00, c0010}: {(r0020, r0030)} >= 0"} ] } ``` - Table `T_01.00` has three monetary cells (`data_type` `m`) in rows 0010 to 0030 of column 0010. Each cell names the `datapoint` that carries its value in the package. - Rule `v1` says row 0010 is the sum of rows 0020 and 0030; rule `v2` says both components are zero or more. For real modules you do not write templates by hand: any tool that reads a DPM release can export them; see [build a template from a DPM release](guides/from-a-dpm-release.md). To write your own, see [write a template by hand](guides/write-a-template.md). ## Generate from the command line ```console $ dpm-synth generate minimal.json -o report.zip --seed 7 rules: 2, passed: 2, failed: 0, not evaluated: 0, not applicable: 0 rounds: 2 (failing rules per round: 1, 0) wrote report.zip (3 facts) $ echo $? 0 ``` The first round found one failing rule (the random total did not match its components), repaired it, and the second round found none. Exit status 0 means every rule that could be evaluated passes; 1 would mean some still fail. The package is written either way. ## Read the package back `report.zip` is an EBA xBRL-CSV report package. Read it with dpm-xbrl: ```python from dpmxbrl import read_report report = read_report("report.zip") print(report.entity_id, report.reference_date, report.filing_indicators) for fact in report.facts: print(fact.template, fact.datapoint, fact.value) print([finding.code for finding in report.filing]) ``` ```text SYNTHETICDPMXL000002.CON 2026-12-31 {'T_01.00': True} t_01.00 dp1001 1126000 t_01.00 dp1002 971000 t_01.00 dp1003 155000 ['invalidFileName'] ``` The entity is the fictional LEI `SYNTHETICDPMXL000002` at the consolidated level. The only EBA filing rule the package breaks is the file name, which for a real submission encodes the entity, module and dates; name the output accordingly if that matters to you. ## Generate from Python The same from Python, with the result of the repair loop: ```python from dpmxbrl import write_xbrl_csv from dpmsynth import generate, load_template template = load_template("minimal.json") result = generate(template, seed=7) print(f"{result.passed} passed, failed: {result.failed}") print(f"rounds: {result.rounds}, failing per round: {result.history}") write_xbrl_csv(result.report, "report.zip") ``` ```text 2 passed, failed: [] rounds: 2, failing per round: [1, 0] ``` `result.report` is a `dpmxbrl.Report`; `result.data` holds the generated values and is a dpm-xl data provider, so you can evaluate any rule on it: ```python from dpmxl import evaluate_rule rule = "with {tT_01.00, c0010}: {r0010} >= {r0020}" print(evaluate_rule(rule, result.data).status) ``` ```text passed ``` ## A bigger template [`sample.json`](samples/sample.json) has a closed table with row and column totals, a ratio and an enumerated cell, an open table keyed by an enumerated property, an open table keyed by LEIs, and a `time_shift` rule that needs last year's report: ```console $ dpm-synth generate sample.json -o sample.zip --seed 7 rules: 9, passed: 8, failed: 0, not evaluated: 1, not applicable: 0 rounds: 5 (failing rules per round: 5, 3, 1, 1, 0) wrote sample.zip (20 facts) ``` The `time_shift` rule is "not evaluated": a single report has no previous period. [The evaluate-and-repair loop](concepts/repair-loop.md) explains the rounds. ## Next steps - [Validate the output](guides/validate-output.md) independently with dpm-xbrl and dpm-xl. - [Load-test](guides/load-testing.md) with millions of open-table rows. - Read [the limits of the loop](concepts/limits.md) before relying on a module's every rule passing. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # How-to guides Recipes for specific tasks. Each assumes you have dpm-synth installed and have generated a first report; the [getting started](../getting-started.md) tutorial covers the basics. Every example runs against the templates in [`docs/samples`](https://github.com/luukhopman/dpm-synth/tree/main/docs/samples). | Guide | Use it to | | --- | --- | | [Write a template by hand](write-a-template.md) | Describe your own tables, keys, data types and rules, and check the template. | | [Build a template from a DPM release](from-a-dpm-release.md) | Export a real module's template from a DPM release, choose its rules, generate and read the results. | | [Load-test with scaled packages](load-testing.md) | Write packages with millions of open-table rows, and know which rules still hold. | | [Reproduce a report with seeds](reproducible-seeds.md) | Get the same bytes again, or different reports on purpose. | | [Validate the output with dpm-xbrl and dpm-xl](validate-output.md) | Check a package independently, as a downstream consumer would. | [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Write a template by hand Templates for real EBA modules come from a DPM tool (see [build a template from a DPM release](from-a-dpm-release.md)). Writing one by hand is useful for your own tables, for a few tables of a module, or for a test fixture. This guide builds a template in Python and writes it as JSON; the [template format](../reference/template-format.md) lists every field. ## The module Start with the format, the release and module codes, and optionally the entry point and the reference date that the package should carry: ```python template = { "format": "dpm-synth-template/1", "release": "1.0", "module": "LOANS", "entry_point": "https://example.com/taxonomy/loans/1.0/mod/loans.json", "reference_date": "2026-06-30", "decimals": {"m": -3}, # monetary amounts in thousands "tables": [], "rules": [], } ``` ## A closed table A closed table has a fixed set of cells. Give each cell an `id`, its row and column codes (without the `r` and `c`), the `datapoint` that carries its value in the package, and its data type: `m` monetary, `p` percentage, `i` integer, `r` decimal, `e` enumerated, `b` boolean, `d` date, anything else text. ```python def cell(table, row, column, datapoint, data_type, **more): return { "id": f"{table}/r{row}/c{column}", "row": row, "column": column, "datapoint": datapoint, "data_type": data_type, **more, } template["tables"].append( { "code": "L_01.00", "cells": [ cell("L_01.00", "0010", "0010", "dp101", "m"), # total loans cell("L_01.00", "0020", "0010", "dp102", "m"), # of which secured cell("L_01.00", "0030", "0010", "dp103", "m"), # of which unsecured cell("L_01.00", "0040", "0010", "dp104", "p"), # share secured cell( "L_01.00", "0050", "0010", "dp105", "e", items=["eba_CT:x1", "eba_CT:x2"], # allowed items, first is the start ), ], } ) ``` ## An open table An open table has one row per key value. Each `open_keys` entry becomes a column of the table's file. A key with `items` is enumerated and takes its items in order; a key without is typed, and gets generated LEI-shaped identifiers. Cells of an open table usually have row `*`, for every row: ```python template["tables"].append( { "code": "L_02.00", "open_keys": [ {"code": "LEI", "column": "c0010"}, # typed: generated identifiers {"code": "qCT", "column": "c0020", "items": ["eba_CT:x1", "eba_CT:x2"]}, ], "cells": [ cell("L_02.00", "*", "0030", "dp201", "m", sheet="*"), # amount cell("L_02.00", "*", "0040", "dp202", "s", sheet="*"), # name ], } ) ``` ## Rules Rules are DPM-XL expressions, as [dpm-xl] parses them. A rule may have a precondition, usually the table's filing indicator; every table of the template counts as reported. ```python template["rules"] = [ { "code": "l1", "expression": "with {tL_01.00, c0010, default: 0, interval: true}: " "{r0010} = {r0020} + {r0030}", "precondition": "{v_L_01.00}", }, {"code": "l2", "expression": "with {tL_01.00, c0010}: {(r0020, r0030)} >= 0"}, { "code": "l3", "expression": "with {default: 0, interval: true}: " "{tL_01.00, r0040, c0010} * {tL_01.00, r0010, c0010} " "= {tL_01.00, r0020, c0010}", }, {"code": "l4", "expression": "{tL_01.00, r0050, c0010} in {[eba_CT:x2]}"}, { "code": "l5", "expression": "with {default: 0, interval: true}: " "sum({tL_02.00, c0030}) <= {tL_01.00, r0010, c0010}", }, {"code": "l6", "expression": "{tL_02.00, c0030} >= 0"}, { "code": "l7", "expression": 'match({tL_02.00, c0030}[get LEI], "^[A-Z0-9]{18}[0-9]{2}$")', }, ] ``` ## Check and generate Write the JSON and load it; `load_template` validates every field and names the first wrong one: ```python import json from pathlib import Path from dpmsynth import generate, load_template Path("loans.json").write_text(json.dumps(template, indent=2), encoding="utf-8") loaded = load_template("loans.json") result = generate(loaded, seed=1) print(result.passed, "passed; failed:", result.failed) for fact in result.report.facts: print(fact.template, fact.datapoint, fact.value, dict(fact.keys)) ``` ```text 7 passed; failed: [] l_01.00 dp101 1451000 {} l_01.00 dp102 583000 {} l_01.00 dp103 868000 {} l_01.00 dp104 0.4018 {} l_01.00 dp105 eba_CT:x2 {} l_02.00 dp201 993000 {'c0010': 'SYNTHEQH524YNG5BY106', 'c0020': 'eba_CT:x1'} l_02.00 dp202 SYNTHB7O259OWOO3SB53 {'c0010': 'SYNTHEQH524YNG5BY106', 'c0020': 'eba_CT:x1'} l_02.00 dp201 433000 {'c0010': 'SYNTHA2ROGUBBB8AYN47', 'c0020': 'eba_CT:x2'} l_02.00 dp202 SYNTH09GLSHV616MTS07 {'c0010': 'SYNTHA2ROGUBBB8AYN47', 'c0020': 'eba_CT:x2'} ``` Monetary amounts are multiples of 1,000, as `"decimals": {"m": -3}` asks, the share in row 0040 times the total gives the secured loans within the precision the package reports, and the item rule picked `eba_CT:x2`. Text cells and typed keys get visibly fictional LEI-shaped values starting with `SYNTH`; only `match` rules with the LEI or `dd/mm/yyyy` pattern ask for anything else. The same file works from the command line: ```console $ dpm-synth generate loans.json -o loans.zip --seed 1 rules: 7, passed: 7, failed: 0, not evaluated: 0, not applicable: 0 rounds: 2 (failing rules per round: 4, 0) wrote loans.zip (9 facts) ``` ## Mistakes the loader catches ```python from dpmsynth import TemplateError broken = json.loads(json.dumps(template)) broken["tables"][1]["open_keys"][0]["column"] = "datapoint" try: load_template(broken) except TemplateError as error: print(error) ``` ```text tables[1].open_keys[0].column: 'datapoint' is a fact column of the table ``` The loader does not check the rules: a rule that does not parse is reported as not evaluated when you generate, with the syntax error, so one bad rule does not stop a module of thousands. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Build a template from a DPM release A real module has thousands of cells and rules, so its template is exported from a DPM release rather than written by hand. The [template format](../reference/template-format.md) is the interface: dpm-synth does not read DPM databases itself, and any tool that can read a release, such as a script over the EBA's DPM database, can write templates. This guide shows what such an exporter must fill in, with a small exporter you can adapt. ## What an exporter fills in For one module of one release, the exporter writes: | Template field | From the DPM release | | --- | --- | | `release`, `module` | The release code, such as `4.3`, and the module code, such as `COREP_OF`. | | `entry_point` | The module's xBRL-CSV entry point (its `.json` URL in the taxonomy), if the consumer of the package needs it. | | `tables[].code` | Every non-abstract table version of the module, such as `C_07.00.a`. | | `tables[].filing_indicator` | The table's template code: the table code without single-letter parts, such as `C_07.00` for `C_07.00.a`. | | `tables[].open_keys` | The table's open-axis properties: their codes, the CSV column of each, and for an enumerated property the item qnames of its domain or subdomain. | | `tables[].cells` | Every reportable cell: its row, column and sheet codes, the datapoint (variable) code that carries its value, the variable's data type code and, for enumerated variables, the allowed item qnames. | | `rules` | The module's validation rules: code, DPM-XL expression and precondition, with a `source` such as `eba`. | | `decimals` | The reporting decimals per data type, if the defaults (`m` 0, `p` 4, `i` 0, `r` 2) do not suit. | Leave out abstract tables, cells without a variable and rules of other modules. Item qnames are written as they appear in a report, such as `eba_qEC:qx16`; dpm-synth writes them back unchanged. ## A small exporter An exporter is a query per field. Here the release's rows are plain Python lists, standing in for the query results of your DPM database: ```python import json from pathlib import Path # Query results of a release, one tuple per row. tables = [("X_01.00", False), ("X_02.00.a", False), ("X_99.00", True)] # code, abstract cells = [ # table, row, column, sheet, variable, data type ("X_01.00", "0010", "0010", None, "500", "m"), ("X_01.00", "0020", "0010", None, "501", "m"), ("X_01.00", "0030", "0010", None, "502", "e"), ("X_02.00.a", "*", "0010", "*", "510", "m"), ] open_axes = [("X_02.00.a", "qCP", None)] # table, property, domain (None: typed) domains = {"CT": ["x1", "x2"]} # domain code -> item codes enumerated = {"502": "CT"} # variable -> domain of an enumerated variable rules = [ # code, expression, precondition ("v1", "with {tX_01.00, c0010, default: 0}: {r0010} >= {r0020}", "{v_X_01.00}"), ( "v2", "with {default: 0}: sum({tX_02.00.a, c0010}) = {tX_01.00, r0020, c0010}", None, ), ("v3", "{tX_01.00, r0030, c0010} in {[eba_CT:x2]}", None), ] def items(domain): return [f"eba_{domain}:{code}" for code in domains[domain]] if domain else None def filing_indicator(code): return ".".join(part for part in code.split(".") if len(part) > 1) template = { "format": "dpm-synth-template/1", "release": "1.0", "module": "X", "reference_date": "2026-12-31", "tables": [ { "code": code, "filing_indicator": filing_indicator(code), "open_keys": [ {"code": prop, "column": prop, "items": items(domain)} for table, prop, domain in open_axes if table == code ], "cells": [ { "id": f"{code}/r{row}/c{column}", "row": row, "column": column, "sheet": sheet, "datapoint": f"dp{variable}", "data_type": data_type, "items": items(enumerated.get(variable)), } for table, row, column, sheet, variable, data_type in cells if table == code ], } for code, abstract in tables if not abstract ], "rules": [ {"source": "eba", "code": code, "expression": expression, "precondition": pre} for code, expression, pre in rules ], } Path("x.json").write_text(json.dumps(template, indent=1), encoding="utf-8") ``` Load the result to check it; `load_template` names the first field that is wrong: ```python from dpmsynth import generate, load_template module = load_template("x.json") print([table.code for table in module.tables], len(module.rules)) result = generate(module, seed=1) print(result.passed, result.failed) ``` ```text ['X_01.00', 'X_02.00.a'] 3 3 [] ``` ## Choose the rules Each rule carries a `source`. If the release, or your own rule set, has rules from more than one source, keep the ones you want before generating. `ModuleTemplate` is a frozen dataclass, so `dataclasses.replace` gives a copy with other rules, or other tables: ```python from dataclasses import replace own = replace(module, rules=tuple(r for r in module.rules if r.source == "eba")) print(len(own.rules)) ``` ```text 3 ``` ## Generate ```console $ dpm-synth generate x.json -o x.zip --seed 1 rules: 3, passed: 3, failed: 0, not evaluated: 0, not applicable: 0 rounds: 2 (failing rules per round: 3, 0) wrote x.zip (5 facts) ``` On EBA DPM 4.3 COREP_OF, with its 1,205 EBA rules, generation takes 15 rounds (about 15 seconds on a laptop), and 999 of the 1,051 rules that apply pass. The other 154 rules do not apply to the generated report: they select no generated cell, such as rows filtered on key values that the generated rows do not have. [Limits of the loop](../concepts/limits.md) has the figures for other modules and explains what remains. ## Read the results The summary names up to 20 failing rules; `--json` lists them all. To see why a rule fails, evaluate it on the generated values and explain the failure with dpm-xl: ```python from dpmxl import evaluate_rule, explain from dpmsynth import generate, load_template template = load_template("sample.json") result = generate(template, seed=7, max_rounds=1) rules = {rule.code: rule for rule in template.rules} for code in result.failed: rule = rules[code] outcome = evaluate_rule( rule.expression, result.data, precondition=rule.precondition ) failure = outcome.failures[0] print(code, explain(outcome.evaluation, rule.expression, failure.key).summary) ``` ```text open 39,000 should not exceed -600,000: difference 639,000, at least 638,999 given the reported precision open-total 332,000 should equal 1,135,000: difference 803,000 rows 1,135,000 should equal -180,000 (717,000 + 75,000 − 972,000): difference 1,315,000, at least 1,314,998 given the reported precision ``` Here the loop was stopped after one round, so rules that more rounds would settle still fail. On a real module, a rule that still fails after the loop stops is usually coupled to another rule that defines the same cell differently; see [limits of the loop](../concepts/limits.md). [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Load-test with scaled packages Loaders and validators need to be tested on reports far larger than any that the repair loop could solve row by row. A scaled package keeps the solved values and multiplies the open tables: every open table gets as many key rows as you ask for, whose values repeat the solved rows under new keys. Tables are streamed into the ZIP file, so memory stays flat however many rows you write. ## From the command line ```console $ dpm-synth generate sample.json -o big.zip --seed 7 --scale-rows 100000 rules: 9, passed: 8, failed: 0, not evaluated: 1, not applicable: 0 rounds: 5 (failing rules per round: 5, 3, 1, 1, 0) wrote big.zip (100018 facts) ``` The summary is that of the solved report; the scaled rows are not evaluated. Table `S_03.00`, keyed by LEIs, got 100,000 rows. Table `S_02.00.a` is keyed only by an enumerated property, so it keeps its two solved rows. On a laptop, a million rows take about six seconds and 35 MB of memory. ## From Python `write_scaled_package` takes one row count for every open table, or a mapping of table code to rows; tables left out of the mapping keep their generated rows: ```python from dpmsynth import generate, load_template, write_scaled_package result = generate(load_template("sample.json"), seed=7) written = write_scaled_package(result.data, "big.zip", rows={"S_03.00": 50_000}) print(written) ``` ```text {'S_01.00': 14, 'S_02.00.a': 4, 'S_03.00': 50000} ``` It returns the fact rows written per table. Read the package back with dpm-xbrl; for millions of facts its `frames` extra reads them into polars frames in seconds: ```python from dpmxbrl import read_report report = read_report("big.zip") print(len(report.facts), report.facts[-1].keys) ``` ```text 50018 {'c0010': 'SYNTHERI000004999908'} ``` ## New keys - A typed key, such as an LEI column, gets identifiers made of `SYNTH`, the first three letters and digits of the key code (padded with `X`), a 10-digit row number and two ISO 17442 check digits, so they are unique, pass LEI validation and are visibly fictional. A table can have up to 9,999,999,999 rows. - An enumerated key keeps the value of the solved row that the scaled row repeats, so the scaled rows have the same mix of items as the solved ones. When every key of a table is enumerated, the table keeps its solved rows; generate with more `--open-rows` to have more item combinations. - Closed tables are written as generated. ## Which rules still hold Row-wise rules, such as "amount at most the exposure" within one row, a `match` on the key, or a rule on the rows with one enumerated key value (`[where CT = [eba_CT:x1]]`), hold on every scaled row, because each row repeats a solved one with its enumerated key values. Rules that add up an open table, such as "the sum over all counterparties equals the total in another table", are not re-solved and will fail: the totals are those of the solved rows. Use the scaled package to test volume, and the unscaled package to test validation results. The package is the same as `dpmxbrl.write_xbrl_csv` would write: the same report package and metadata files, the root folder named as the ZIP file, and the same CSV layout. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Reproduce a report with seeds Every random choice dpm-synth makes, starting values and generated identifiers alike, comes from one random generator seeded with `--seed` (default 0). The written package has fixed ZIP timestamps and a fixed order of files and facts. So the same inputs give the same bytes: ```python import hashlib from pathlib import Path from dpmxbrl import write_xbrl_csv from dpmsynth import generate, load_template template = load_template("sample.json") for folder, seed in (("first", 7), ("second", 7), ("other", 8)): Path(folder).mkdir() write_xbrl_csv(generate(template, seed=seed).report, f"{folder}/report.zip") def digest(folder): return hashlib.sha256(Path(folder, "report.zip").read_bytes()).hexdigest() print(digest("first") == digest("second")) print(digest("first") == digest("other")) ``` ```text True False ``` ## What the output depends on The package is a function of: - the template, including the order of its tables, cells and rules; - the seed and the other options: `--open-rows`, `--max-rounds`, `--entity`, `--level`, `--reference-date` and `--scale-rows`; - the package's file name, which names its root folder (the example writes each package as `report.zip`, in its own folder); - the versions of dpm-synth, dpm-xl and dpm-xbrl. It does not depend on the machine, the time, the working directory or Python's hash seed. A patch or minor release of any of the three packages may change the values generated for a seed, for example when a repair or a rule's evaluation improves; the [changelog](../project/changelog.md) says so when it does. ## Several different reports Use different seeds for reports that differ, such as one report per test case or a set of reports for a performance test: ```console $ dpm-synth generate minimal.json -o report-1.zip --seed 1 rules: 2, passed: 2, failed: 0, not evaluated: 0, not applicable: 0 rounds: 2 (failing rules per round: 1, 0) wrote report-1.zip (3 facts) $ dpm-synth generate minimal.json -o report-2.zip --seed 2 rules: 2, passed: 2, failed: 0, not evaluated: 0, not applicable: 0 rounds: 2 (failing rules per round: 1, 0) wrote report-2.zip (3 facts) ``` The values differ; on a large module, the number of rounds and the rules that still fail may differ from seed to seed too. ## Stable test fixtures To keep a test fixture stable across upgrades, commit the generated package rather than regenerating it in every test run, and regenerate it on purpose, with the same seed, when you upgrade. A test that only needs a valid report of a module can regenerate it each time and check the rule results instead of the bytes. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Validate the output with dpm-xbrl and dpm-xl dpm-synth evaluates the rules on its own values while it generates. To be sure the package says the same, check it as a downstream consumer would: read the written ZIP file with [dpm-xbrl], and evaluate the template's rules on what was read with [dpm-xl]. The test suite does exactly this for every sample template. ## Generate and read back ```python from dpmxbrl import read_report, write_xbrl_csv from dpmsynth import generate, load_template template = load_template("sample.json") result = generate(template, seed=7) write_xbrl_csv(result.report, "sample.zip") report = read_report("sample.zip") print(report.format, report.entity_id, len(report.facts)) for finding in report.filing: print(finding.severity, finding.rule, finding.code) ``` ```text xbrl-csv SYNTHETICDPMXL000002.CON 20 warning File naming invalidFileName ``` dpm-xbrl records every [EBA filing rule][dpm-xbrl-filing] the file breaks. The only one is the file name, which for a real submission encodes the entity, module and creation time. ## A provider over the package dpm-xl evaluates rules against a data provider: an object that answers a selection such as `{tS_01.00, r0010, c0010}` with data points. The facts of an EBA xBRL-CSV package carry a datapoint, not row and column codes, so the template's cells place them. This provider knows nothing of dpm-synth's own values; everything comes from the package (see [writing a provider][dpm-xl-provider] for the protocol): ```python from datetime import date from decimal import Decimal from dpmxl import DataPoint, Item, Number, axis_matches DECIMALS = { "m": "decimalsMonetary", "p": "decimalsPercentage", "i": "decimalsInteger", "r": "decimalsDecimal", } def selected(code, selector): if selector is None or selector.wildcard: return True return code not in (None, "*") and axis_matches(code, selector) class PackageProvider: """Facts of a report read with dpm-xbrl, placed by a template's cells.""" def __init__(self, template, report): self.template, self.report = template, report self.rows = {} # table code -> key row -> datapoint -> value for fact in report.facts: table = template.table(fact.template) cell = next(c for c in table.cells if c.datapoint == fact.datapoint) columns = {key.column: key for key in table.open_keys} keys = tuple( (columns[name].code, Item.parse(text) if columns[name].items else text) for name, text in fact.keys.items() ) row = self.rows.setdefault(table.code, {}).setdefault(keys, {}) row[fact.datapoint] = self.value(cell.data_type, fact.value) def value(self, data_type, text): if text is None: return None if data_type in DECIMALS: # reported decimals become the tolerance decimals = self.report.decimals_parameters.get(DECIMALS[data_type]) return Number.of(Decimal(text), decimals) if data_type == "e": return Item.parse(text) if data_type == "b": return text == "true" if data_type == "d": return date.fromisoformat(text) return text def select(self, query): table = self.template.table(query.table) if table is None: raise LookupError(f"table {query.table} is not in the template") return [ DataPoint( table=table.code, row=cell.row, column=cell.column, sheet=cell.sheet, keys=dict(keys), value=values.get(cell.datapoint), ref=(table.code, cell.datapoint, keys), ) for keys, values in self.rows.get(table.code, {}).items() for cell in table.cells if selected(cell.row, query.rows) and selected(cell.column, query.columns) and selected(cell.sheet, query.sheets) ] def table_reported(self, table): folded = table.casefold() return any( reported and code.casefold() == folded for code, reported in self.report.filing_indicators.items() ) or any( t.code.casefold() == folded and t.code in self.rows for t in self.template.tables ) def parameter(self, name): if name.casefold() == "refperiod": return self.report.reference_date if name.casefold() == "reportinglevel": return self.report.entity_id.rsplit(".", 1)[-1].casefold() return None def open_keys(self, table): found = self.template.table(table) return tuple(key.code for key in found.open_keys) if found else () ``` ## Evaluate the rules ```python from collections import Counter from dpmxl import evaluate_rule provider = PackageProvider(template, report) statuses = { rule.code: evaluate_rule( rule.expression, provider, precondition=rule.precondition ).status for rule in template.rules } print(Counter(statuses.values())) failed = [code for code, status in statuses.items() if status == "failed"] print(failed == result.failed) ``` ```text Counter({'passed': 8, 'unsupported': 1}) True ``` The package passes the same eight rules. The ninth, a `time_shift`, is `unsupported` here, as it is "not evaluated" in the generator: a single report has no previous period. To evaluate it, give the provider an `earlier_report(period, count)` method that returns a provider over last year's package. For a large module, use dpm-xl's columnar evaluator and its [end-to-end guide](https://luukhopman.github.io/dpm-xl/guides/end-to-end/), which reads a package with dpm-xbrl in the same way. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Concepts These pages explain how dpm-synth works and why it behaves as it does. For tasks, see the [how-to guides](../guides/index.md); for exact behaviour, the [reference](../reference/index.md). | Page | Explains | | --- | --- | | [The evaluate-and-repair loop](repair-loop.md) | Starting values, bounds and definitions, the repairs, rounds and rounding. | | [Limits of the loop](limits.md) | Results on real EBA modules, the rules that stay failing or do not apply, and why. | [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # The evaluate-and-repair loop `generate` does not solve a module's rules as a system of equations. It starts from plausible values, evaluates every rule with [dpm-xl], and changes one cell for each failing record so that the failing comparison holds; then it evaluates again. Most rules in EBA modules are local (a total is the sum of its components, an amount is not negative), so this converges in a few rounds for most of a module. ## 1. Starting values Every cell gets a value for its [data type](../reference/template-format.md#data-types): monetary amounts in thousands, ratios between 0 and 1, small integers, the first allowed item of an enumerated cell, the reference date for dates and an LEI-shaped identifier for text. Every open table gets `open_rows` key rows: allowed items for enumerated keys, LEI-shaped identifiers for typed keys. Every generated LEI-shaped identifier starts with `SYNTH` (`LEI_PREFIX`), followed by 13 random letters and digits and two valid ISO 17442 check digits: it passes LEI validation, but is visibly fictional and cannot coincide with a real entity's LEI. The reporting entity is the fictional `SYNTHETICDPMXL000002` unless you pass another. With `max_rounds=0` you see the starting values unrepaired: ```python from dpmsynth import generate, load_template template = load_template("minimal.json") start = generate(template, seed=7, max_rounds=0) print([fact.value for fact in start.report.facts], start.failed) ``` ```text ['332000', '971000', '155000'] ['v1'] ``` The total in row 0010 is not the sum of rows 0020 and 0030, so rule `v1` fails. The first round repairs it by setting the total, the cell that `v1` defines, to 1,126,000; the second round finds no failing rule. ## 2. Analysis Before the first round, the rules are read once: - **Bounds.** A rule that compares cells with a constant, such as `{(r0020, r0030)} >= 0` or `{c0020} <= 0`, becomes a bound on each of those cells, and so does such a comparison joined to the rest of a rule by `and`. Starting values outside a bound change sign, or take the bound; repairs never move a cell outside its bounds. A strict bound (`> 0`) is one reporting step past the constant, such as 1 for an amount or 0.0001 for a percentage. - **Definitions.** A rule whose `=` has a single cell on one side, such as `{r0010} = {r0020} + {r0030}`, *defines* that cell. Repairs of other rules prefer to change cells that no other rule defines, so a total that one rule owns is not pulled around by rules that merely use it. - **Order.** Rules are sorted so that a rule defining a component runs before the rule that sums the components: totals are then settled from components that no longer change in that round. ## 3. Repairs Each failing record of a rule is traced to the comparison that fails, using the evaluation trace that dpm-xl keeps. Its repair depends on the construct: | Construct | Repair | | --- | --- | | `a = b`, `a <= b`, `a > b`, ... on numbers | Expand `a - b` into terms `coefficient × cell`: sums, differences, unary minus, constants, `sum(...)`, `abs` (at its current sign), a product of two cells (each at the other's current value) and division by a constant or cell. One cell absorbs the whole difference, the first that no other rule defines and whose new value respects its bounds. Strict comparisons are met by one reporting step. | | `a = b` on items, text or dates | Copy the known side into the cells of the other. | | `and`, `or`, `not` | Repair both sides of `and`; the first side of `or` that can be changed; `not` repairs its operand towards the opposite. | | `if c then a else b endif` | Repair the branch that the condition selects. | | `isnull(x)` | Clear `x`; `not isnull(x)` gives a cleared cell a starting value again. | | `x in {…}` or `x in s` | Set `x` to the first member of the set that fits its cell: a number for a numeric cell, an item for an enumerated one. Any set works: a literal, a `set-*` parameter, `union(...)`, or a selection of cells. | | `match(x, pattern)` | For the LEI pattern `[A-Z0-9]{18}[0-9]{2}`, a checksum-valid LEI-shaped identifier starting with `SYNTH`; for `dd/mm/yyyy`, the reference date. | Numbers never go into text or enumerated cells and nothing else goes into a numeric cell. A repair that would need a number beyond 10^15, as a ratio dividing by an almost-zero cell can, is not made. ## 4. Rounds and stopping A round evaluates every rule and repairs every failing record. The loop stops when no rule fails, when a round changes nothing, after `max_rounds` (default 40), or when the failing count has not reached a new low for six rounds: rules that keep undoing each other's repairs. The failing count per round is `GenerationResult.history`: ```python result = generate(load_template("sample.json"), seed=7) print(result.history, result.rounds, result.repairs) ``` ```text [5, 3, 1, 1, 0] 5 16 ``` ## 5. Rounding and the final evaluation Repairs work on values with eight decimals. At the end, every number is rounded to its reporting decimals (`m` 0, `p` 4, `i` 0, `r` 2 unless the template says otherwise), and every rule is evaluated once more on the rounded values. That final evaluation is what `passed`, `failed`, `not_evaluated` and `not_applicable` report. The generated values are evaluated as the written package will be: each number carries its reporting decimals, which dpm-xl turns into a tolerance for rules with `interval: true`. A ratio rounded to four decimals is judged as a validator would judge the reported ratio, so a rule that passes here passes on the package; the [validation guide](../guides/validate-output.md) checks this. ## Why not a solver A complete approach would solve a module's linear equalities jointly, for example with a sparse least-squares or a mixed-integer solver, and then repair the rest. The repair loop needs no solver dependency, handles the non-linear and non-numeric constructs uniformly, and satisfies most rules of real modules in seconds. The template carries everything a solver would need, so one can be added without changing the format. [Limits of the loop](limits.md) shows what remains. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Limits of the loop The [evaluate-and-repair loop](repair-loop.md) changes one cell at a time to fix one failing comparison. That settles most rules of real modules, but not all: some rules constrain the same cells in ways that local repairs cannot reconcile. A report that passes every rule is therefore not guaranteed; the result always says which rules still fail. ## Results on EBA modules EBA DPM 4.3 modules, their EBA validation rules only, seed 7, default options, dpm-synth 1.0.0 with dpm-xl and dpm-xbrl 1.0.0. Times are wall clock on a laptop (AMD Ryzen 7 4700U) and only indicate the scale; the other columns do not depend on the machine: | Module | Tables | Cells | Rules | Apply | Pass | Fail | Not applicable | Not evaluated | Rounds | Time | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | COREP_OF | 81 | 21,339 | 1,205 | 1,051 | 999 | 52 | 154 | 0 | 15 | 13 s | | FINREP9 | 124 | 13,834 | 2,260 | 2,258 | 1,929 | 329 | 0 | 2 | 14 | 10 s | | AE | 23 | 1,306 | 218 | 218 | 207 | 11 | 0 | 0 | 10 | < 1 s | | COREP_LCR_DA | 14 | 5,119 | 328 | 328 | 321 | 7 | 0 | 0 | 9 | 2 s | | COREP_LE | 5 | 79 | 25 | 25 | 24 | 1 | 0 | 0 | 9 | < 1 s | "Apply" counts the rules that are evaluated and whose precondition holds. On each of these modules, the rules that pass and fail on the generated values are exactly those that pass and fail when the written package is read back with dpm-xbrl and evaluated with dpm-xl, as in [validate the output](../guides/validate-output.md). ## Rules that still fail - **A cell defined twice.** Two rules give the same cell different definitions, such as a capital amount as the sum of its components and as a ratio times the total risk exposure, while the ratio has its own minimum. Each repair undoes the other; the loop stops when the failing count no longer improves. - **Totals over open tables that are also constrained per row.** A sum over all rows of an open table must equal a total, and each row is bounded by other rules; the repair of the sum moves one row past its own bounds, or cannot. - **Chains of inequalities.** `a <= b`, `b <= c` and `c <= a + d`, with some of the cells defined by sums elsewhere, can need several cells to move together. - **Precision.** A rule without `interval: true` that multiplies a ratio by an amount holds before rounding but can miss by a few units after the ratio is rounded to its reporting decimals. [Build a template from a DPM release](../guides/from-a-dpm-release.md#read-the-results) shows how to explain each failing rule with dpm-xl. ## Rules that are not evaluated `not_evaluated` holds the rules dpm-xl reports as an error or as unsupported, with the reason: - `time_shift` compares with an earlier period, and a single report has none; - a rule on a table that is not in the module's template; - a rule that does not parse, or uses a construct dpm-xl does not support. They do not count towards the exit status. ## Rules that do not apply `not_applicable` holds the rules that the generated report gives nothing to check: the precondition is false, or the rule selects no generated cell. On COREP_OF these are mostly rules that filter an open table on key values, such as `[where CEG = [eba_GA:qx2014]]`, when the generated key rows use other items. More rows (`--open-rows`) cover more items, at the cost of time: enumerated keys take their items in order. ## Regular expressions `match` is repaired for the two patterns EBA rules use, LEIs and `dd/mm/yyyy` dates. Any other pattern is evaluated but not repaired. ## What would do better Solving a module's linear equalities jointly, with a sparse least-squares or mixed-integer solver, before repairing the rest would settle most cells defined twice consistently, or prove that the rules contradict each other. The template already carries what such a solver needs. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Reference Exact, complete descriptions. For explanations, see the [concepts](../concepts/index.md); for recipes, the [how-to guides](../guides/index.md). | Page | Describes | | --- | --- | | [Template format](template-format.md) | The `dpm-synth-template/1` JSON format, field by field, and how it is validated. | | [CLI](cli.md) | The `dpm-synth generate` command and every option. | | [Exit statuses](exit-statuses.md) | What each exit status means, and when the package is written. | | [JSON output](json-output.md) | The layout of `dpm-synth generate --json`, with `schema_version`. | | [Python API](api.md) | Every public name, generated from the docstrings. | [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Template format A template describes one module of a DPM release: its tables, the cells of each table, and the rules a report must satisfy. It is a JSON document (UTF-8) in the `dpm-synth-template/1` format, read by `load_template`. Any DPM source can produce one; see [build a template from a DPM release](../guides/from-a-dpm-release.md) and [write a template by hand](../guides/write-a-template.md). ## Example ```json { "format": "dpm-synth-template/1", "release": "4.3", "module": "COREP_OF", "entry_point": "http://www.eba.europa.eu/eu/fr/xbrl/crr/fws/corep/4.3/mod/corep_of.json", "reference_date": "2026-12-31", "decimals": {"m": 0, "p": 4, "i": 0, "r": 2}, "tables": [ { "code": "C_07.00.a", "filing_indicator": "C_07.00", "open_keys": [ {"code": "qEBB", "column": "qEBB", "items": ["eba_qEC:qx16", "eba_qEC:qx23"]} ], "cells": [ {"id": "table-cell:5", "row": "0010", "column": "0010", "sheet": "*", "datapoint": "dp12345", "data_type": "m"} ] } ], "rules": [ {"source": "eba", "code": "v0001_m", "expression": "…", "precondition": "{v_C_07.00}"} ] } ``` ## The module | Field | Type | Required | Meaning | | --- | --- | --- | --- | | `format` | string | yes | Exactly `"dpm-synth-template/1"`. | | `release` | string | yes | The DPM release, such as `"4.3"`. Informative. | | `module` | string | yes | The module code, such as `"COREP_OF"`. Named in error messages and the JSON output. | | `entry_point` | string or null | no | The module's xBRL-CSV entry point, written to `reports/report.json` as `extends`. | | `reference_date` | string or null | no | The reference date, `YYYY-MM-DD`; it is the `refPeriod` parameter, the value of `d` cells and the rules' `{refPeriod}`. Default `2026-12-31`; the `--reference-date` option overrides it. | | `decimals` | object | no | Reporting decimals by data type code, such as `{"m": -3}`; whole numbers from -20 to 20. Unlisted types keep the defaults `m` 0, `p` 4, `i` 0, `r` 2. | | `tables` | array | yes | At least one [table](#tables). | | `rules` | array | no | The [rules](#rules); none means every value is simply generated. | ## Tables | Field | Type | Required | Meaning | | --- | --- | --- | --- | | `code` | string | yes | The table code, such as `"C_07.00.a"`, as rules name it. Lower-cased, it names the table's file, `reports/c_07.00.a.csv`, so it is letters, digits, `_`, `.` and `-`, starting with a letter or digit, and not `parameters` or `FilingIndicators`. Unique within the template, ignoring case. | | `filing_indicator` | string or null | no | The filing indicator the table is reported under, such as `"C_07.00"`. Default: the code. Every filing indicator of the template is reported as `true`. | | `open_keys` | array | no | The table's [open keys](#open-keys); none for a closed table. | | `cells` | array | yes | At least one [cell](#cells). | ## Open keys An open table has one row per combination of key values, such as one row per counterparty. Each key becomes a column of the table's file. | Field | Type | Required | Meaning | | --- | --- | --- | --- | | `code` | string | yes | The key's property code, as rules name it, such as `"qEBB"` in `[where qEBB = …]`. | | `column` | string or null | no | The key's column in the table's file. Default: the code. Unique within the table, and not `datapoint` or `factValue`. | | `items` | array of strings or null | no | The allowed item qnames of an enumerated key, such as `"eba_qEC:qx16"`. Null or empty for a typed key, whose values are generated LEI-shaped identifiers starting with `SYNTH`. | The generator gives every open table `open_rows` key rows (default 2). Enumerated keys take their items in order, row by row, so an enumerated key with fewer items than rows gives fewer distinct rows. ## Cells | Field | Type | Required | Meaning | | --- | --- | --- | --- | | `id` | string | yes | An identifier of the cell, unique within the template. Only used in error messages. | | `datapoint` | string | yes | The datapoint code written in the table's `datapoint` column, such as `"dp12345"`. Two tables may share a datapoint; they then share its value. | | `data_type` | string or null | no | The [data type](#data-types). | | `row` | string or null | no | The row code, such as `"0010"`, without the `r`. Null for a table without rows; `"*"` for a cell of every row of an open table, which only a selection without rows, or with `r*`, selects. | | `column` | string or null | no | The column code, such as `"0010"`. | | `sheet` | string or null | no | The sheet code; `"*"` for a cell on every sheet, as in open tables. | | `items` | array of strings or null | no | The allowed item qnames of an enumerated cell. The first is its starting value. | Rules select cells by table, row, column and sheet, as in `{tC_07.00.a, r0010, c0010}`; the generator answers such a selection with the template's cells, for every key row of an open table. ## Data types `data_type` is a DPM data type code. It decides the starting value, which repairs apply, how the value is rounded and how it is written. | Code | Type | Starting value | Written as | | --- | --- | --- | --- | | `m` | monetary | a multiple of 1,000 from 1,000 to 1,000,000 | a decimal with `decimals.m` places | | `p` | percentage | 0.01 to 0.99 | a decimal with `decimals.p` places | | `i` | integer | 1 to 50 | a decimal with `decimals.i` places | | `r` | decimal | 1.00 to 9.99 | a decimal with `decimals.r` places | | `e` | enumerated | the first of `items`, else `eba_qx:qx0` | the item qname as the template writes it | | `b` | boolean | `true` | `true` or `false` | | `d` | date | the reference date | `YYYY-MM-DD` | | anything else, or null | text | a random LEI-shaped identifier starting with `SYNTH` | the text | Only numeric cells (`m`, `p`, `i`, `r`) take part in arithmetic repairs. Negative decimals round to tens, hundreds or thousands: with `"m": -3` a monetary value is written as a multiple of 1,000. ## Rules | Field | Type | Required | Meaning | | --- | --- | --- | --- | | `code` | string | yes | The rule code, such as `"v0001_m"`. Unique within the template: results list rules by code. | | `expression` | string | yes | The DPM-XL expression, as [dpm-xl] parses it. | | `precondition` | string or null | no | A DPM-XL precondition, such as `"{v_C_07.00}"`; the rule applies only when it holds. | | `source` | string or null | no | Who defines the rule, such as `"eba"`. Default `"eba"`. Informative: filter the rules before generating if you want only some sources. | Every table of the template counts as reported, so a precondition such as `{v_C_07.00}` holds when the template has that table or filing indicator. `{vReportingLevel}` is `con` or `ind` and `{refPeriod}` is the reference date. A rule that does not parse is not an error of the template: it is reported as not evaluated, with the syntax error. ## Validation `load_template` checks every field above and raises `TemplateError` for the first problem, naming the field by its path. Text that cannot be written as UTF-8, such as a lone surrogate escape `\ud800` in the JSON, is rejected too, since the package is UTF-8: ```python import json from pathlib import Path from dpmsynth import TemplateError, load_template document = json.loads(Path("minimal.json").read_text(encoding="utf-8")) document["tables"][0]["cells"][1]["datapoint"] = 1002 try: load_template(document) except TemplateError as error: print(error) ``` ```text tables[0].cells[1].datapoint: expected a non-empty string, got the number 1002 ``` A file that cannot be read or is not JSON raises `TemplateError` too, and the command line reports any of them with exit status 3. ## Compatibility Fields a version does not know are ignored, so a template with extra fields loads, and exports may carry more information than dpm-synth uses. A change that a reader of `dpm-synth-template/1` cannot ignore gets a new format version, `dpm-synth-template/2`; see [stability and versioning](../project/stability.md). [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # CLI Installing dpm-synth installs the `dpm-synth` command, with one subcommand, `generate`. `uvx dpm-synth` runs it without installing. ```console $ dpm-synth --help usage: dpm-synth [-h] [--version] {generate} ... Generate synthetic EBA xBRL-CSV reports that satisfy a module's DPM-XL validation rules. positional arguments: {generate} generate write an xBRL-CSV report package for a template options: -h, --help show this help message and exit --version show program's version number and exit examples: dpm-synth generate template.json -o report.zip dpm-synth generate template.json -o report.zip --seed 7 --open-rows 3 dpm-synth generate template.json -o report.zip --json dpm-synth generate template.json -o big.zip --scale-rows 1000000 exit statuses: 0 every evaluable rule passes, 1 some rules still fail (the package is written), 2 usage error, 3 unreadable or invalid template, or the package cannot be written. The template format: https://luukhopman.github.io/dpm-synth/reference/template-format/ ``` ## `dpm-synth generate` ```console $ dpm-synth generate --help usage: dpm-synth generate [-h] -o ZIP [--seed N] [--open-rows N] [--max-rounds N] [--scale-rows N] [--reference-date YYYY-MM-DD] [--entity LEI] [--level {CON,IND}] [--json] template Generate values for every cell of a template, repair them until the rules pass, and write an xBRL-CSV report package. positional arguments: template dpm-synth-template/1 JSON file options: -h, --help show this help message and exit -o, --output ZIP report package to write (.zip) --seed N random seed (default 0) --open-rows N key rows per open table, at most 1000 (default 2) --max-rounds N most evaluate-and-repair rounds (default 40) --scale-rows N after solving, write every open table with N key rows (load testing; rules over open-table totals will fail) --reference-date YYYY-MM-DD reference date (default: the template's, else 2026-12-31) --entity LEI LEI of the reporting entity (default SYNTHETICDPMXL000002) --level {CON,IND} reporting level, the entity suffix (default CON) --json print a JSON summary ``` `generate` loads the [template](template-format.md), runs `generate` and writes the report with `dpmxbrl.write_xbrl_csv`, or with `write_scaled_package` for `--scale-rows`. It prints a summary: ```console $ dpm-synth generate sample.json -o sample.zip --seed 7 rules: 9, passed: 8, failed: 0, not evaluated: 1, not applicable: 0 rounds: 5 (failing rules per round: 5, 3, 1, 1, 0) wrote sample.zip (20 facts) ``` - `rules` counts the template's rules; `passed`, `failed`, `not evaluated` (dpm-xl could not evaluate the rule, such as a `time_shift`) and `not applicable` (its precondition is false, or it selects no cell) add up to it. - `rounds` lists the failing rules at the start of each round. - When rules still fail, a line `still failing: ...` names up to 20 of them. - `wrote` names the package and the facts in it. | Option | Effect | | --- | --- | | `-o`, `--output ZIP` | The package to write; required, and must end in `.zip`. Its name without `.zip` names the package's root folder. An existing file is replaced. | | `--seed N` | Random seed, any whole number. The same template, seed, options and versions give the same package; see [reproduce a report](../guides/reproducible-seeds.md). | | `--open-rows N` | Key rows per open table while solving, from 1 to 1,000 (`MAX_OPEN_ROWS`). A table whose keys are all enumerated gets at most as many rows as its item lists repeat after. | | `--max-rounds N` | Most evaluate-and-repair rounds, 0 or more; 0 writes the starting values. | | `--scale-rows N` | Write every open table with `N` key rows, from 1 to 9,999,999,999, by repeating the solved rows under new keys; see [load-test with scaled packages](../guides/load-testing.md). The summary and the exit status are those of the unscaled report. | | `--reference-date YYYY-MM-DD` | Overrides the template's reference date; exactly `YYYY-MM-DD`, as in the template. | | `--entity LEI` | The reporting entity, letters and digits (`.`, `_` and `-` allowed). The entity identifier is `.` in the reporting-subject scheme. | | `--level CON\|IND` | The reporting level, consolidated or individual. | | `--json` | Print the summary as JSON instead; see [JSON output](json-output.md). | | `--version` | Print the version and exit. | ## Errors Errors are one line on standard error, starting with `error:`, and never a traceback: ```console $ dpm-synth generate missing.json -o report.zip error: missing.json: cannot read: No such file or directory $ echo $? 3 $ dpm-synth generate minimal.json -o report.csv usage: dpm-synth generate [-h] -o ZIP [--seed N] [--open-rows N] [--max-rounds N] [--scale-rows N] [--reference-date YYYY-MM-DD] [--entity LEI] [--level {CON,IND}] [--json] template dpm-synth generate: error: argument -o/--output: expected a .zip file, got 'report.csv' $ echo $? 2 ``` See [exit statuses](exit-statuses.md) for every status. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Exit statuses | Status | Meaning | Package written | | --- | --- | --- | | 0 | Every rule that could be evaluated passes. Rules that are not evaluated (such as `time_shift`) or not applicable do not count. | yes | | 1 | Some rules still fail; the summary names them. | yes | | 2 | Usage error: an unknown option, a missing argument, or a bad option value, such as `--open-rows 0`, `--open-rows 5000`, a reference date not written as `YYYY-MM-DD` or an output not ending in `.zip`. argparse prints the usage and the error. | no | | 3 | The template cannot be read, is not JSON or is not a valid [template](template-format.md); or the package cannot be written. One line `error: ...` on standard error. | no | The statuses match the sibling commands: 2 is always a usage error, and neither tool prints a traceback for bad input. With `--scale-rows`, the status is that of the unscaled report, since the scaled rows are not evaluated. A rule that cannot be satisfied makes status 1, not a failure of the run: the package is still useful for testing. To accept a known set of failing rules in CI, compare the `failed` list of the [JSON output](json-output.md) with the list you expect instead of relying on the status: ```console $ dpm-synth generate sample.json -o sample.zip --seed 7 --json --max-rounds 1 { "schema_version": 1, "output": "sample.zip", "release": "1.0", "module": "SAMPLE", "seed": 7, "rules": 9, "passed": 5, "failed": [ "open", "open-total", "rows" ], "not_evaluated": { "history": "The rule compares with the previous reporting period (time_shift), which a single report does not include" }, "not_applicable": [], "rounds": 1, "history": [ 5 ], "repairs": 10, "facts": 20 } $ echo $? 1 ``` [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # JSON output `dpm-synth generate --json` prints one JSON object (UTF-8, two-space indentation) instead of the text summary. The package is written and the [exit status](exit-statuses.md) is the same as without `--json`. ```console $ dpm-synth generate sample.json -o sample.zip --seed 7 --json { "schema_version": 1, "output": "sample.zip", "release": "1.0", "module": "SAMPLE", "seed": 7, "rules": 9, "passed": 8, "failed": [], "not_evaluated": { "history": "The rule compares with the previous reporting period (time_shift), which a single report does not include" }, "not_applicable": [], "rounds": 5, "history": [ 5, 3, 1, 1, 0 ], "repairs": 16, "facts": 20 } ``` ## Fields | Field | Type | Meaning | | --- | --- | --- | | `schema_version` | integer | Version of this layout, currently `1`. | | `output` | string | The package written, as given to `--output`. | | `release` | string | The template's `release`. | | `module` | string | The template's `module`. | | `seed` | integer | The random seed. | | `rules` | integer | Rules in the template. | | `passed` | integer | Rules that pass on the written values. | | `failed` | array of strings | Codes of the rules that still fail, in the order they were last evaluated. | | `not_evaluated` | object | Rule code to the reason dpm-xl could not evaluate the rule, in template order. | | `not_applicable` | array of strings | Codes of the rules whose precondition is false or that select no cell. | | `rounds` | integer | Evaluate-and-repair rounds run. | | `history` | array of integers | Failing rules at the start of each round. | | `repairs` | integer | Failing records repaired over all rounds. | | `facts` | integer | Facts written; with `--scale-rows`, those of the scaled package. | `passed`, the lengths of `failed`, `not_evaluated` and `not_applicable` add up to `rules`. The fields are those of `GenerationResult`. ## Compatibility `schema_version` changes only with a new major version of dpm-synth. Within a major version, fields are not removed or renamed and their types do not change; new fields may be added, so ignore fields you do not know. The `not_evaluated` reasons come from dpm-xl and may be reworded. See [stability and versioning](../project/stability.md). [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Python API The public API is the names in `dpmsynth.__all__`, all on this page and covered by the [stability policy](../project/stability.md). They are defined in `dpmsynth.template`, `dpmsynth.generator` and `dpmsynth.scale`; import them from `dpmsynth`. Names that start with an underscore, such as the private methods of `SyntheticData`, are internal. ```python from dpmxbrl import write_xbrl_csv from dpmsynth import generate, load_template, write_scaled_package result = generate(load_template("sample.json"), seed=7) write_xbrl_csv(result.report, "report.zip") written = write_scaled_package(result.data, "big.zip", rows=10_000) print(result.passed, result.failed, written) ``` ```text 8 [] {'S_01.00': 14, 'S_02.00.a': 4, 'S_03.00': 10000} ``` ## Thread safety `generate` keeps no shared state, so several templates may be generated in parallel threads; it is CPU-bound, so processes run faster. A `SyntheticData` is changed while it is generated and is safe to read from several threads afterwards; templates are immutable. ## Templates ## `dpmsynth.load_template` ```python def load_template(source: str | os.PathLike[str] | Mapping[str, Any]) -> ModuleTemplate ``` Load and validate a template from a JSON file or a parsed document. Fields this version does not know are ignored, so newer exports still load. Args: source: A path to a JSON file, or the document as a mapping. Returns: ModuleTemplate: The validated template. Raises: TemplateError: If the file cannot be read, is not JSON, or the document is not a valid ``dpm-synth-template/1`` template. Examples: >>> template = load_template("minimal.json") >>> template.module, len(template.tables), len(template.rules) ('DEMO', 1, 2) ## `dpmsynth.ModuleTemplate` ```python class ModuleTemplate(release: str, module: str, entry_point: str | None, tables: tuple[TemplateTable, ...], rules: tuple[TemplateRule, ...], reference_date: date | None = None, decimals: Mapping[str, int] = dict()) ``` Everything needed to generate one module's report. Build one with `load_template`. Attributes: release: The DPM release, such as ``4.3``. module: The module code, such as ``COREP_OF``. entry_point: The module's entry point, written to ``report.json``. tables: The module's tables. rules: The rules the report should satisfy. reference_date: The report's reference date; 2026-12-31 when ``None``. decimals: Reporting decimals by data type code; ``m`` 0, ``p`` 4, ``i`` 0 and ``r`` 2 unless given. ## `dpmsynth.TemplateTable` ```python class TemplateTable(code: str, filing_indicator: str, cells: tuple[TemplateCell, ...], open_keys: tuple[OpenKey, ...] = ()) ``` A table, its filing indicator, open keys and cells. Attributes: code: The table code, such as ``C_07.00.a``; it names the CSV file. filing_indicator: The filing indicator the table is reported under. cells: The reportable cells. open_keys: The open-axis key columns; empty for a closed table. ## `dpmsynth.OpenKey` ```python class OpenKey(code: str, column: str, items: tuple[str, ...] | None = None) ``` An open-axis key column of a table. Attributes: code: The key's property code, as rules name it, such as ``qEBB``. column: The column of the key in the table's CSV file. items: Allowed item qnames for an enumerated key; ``None`` for a typed key, which gets generated identifiers. ## `dpmsynth.TemplateCell` ```python class TemplateCell(id: str, datapoint: str, data_type: str | None, row: str | None = None, column: str | None = None, sheet: str | None = None, items: tuple[str, ...] | None = None) ``` One reportable cell and the datapoint that carries its value. Attributes: id: An identifier of the cell, unique within the template. datapoint: The datapoint code written in the ``datapoint`` column. data_type: The DPM data type code, such as ``m`` or ``e``; ``None`` or an unknown code is text. row: The row code, such as ``0010``; ``None`` for a table without rows, ``*`` for every row of an open table. column: The column code. sheet: The sheet code, ``*`` for every sheet, or ``None``. items: Allowed item qnames of an enumerated cell. ## `dpmsynth.TemplateRule` ```python class TemplateRule(code: str, expression: str, precondition: str | None = None, source: str = 'eba') ``` A validation rule with its precondition. Attributes: code: The rule code, such as ``v0001_m``. expression: The DPM-XL expression. precondition: An optional DPM-XL precondition, such as ``{v_C_01.00}``. source: Who defined the rule, such as ``eba``. ## `dpmsynth.TemplateError` ```python class TemplateError(ValueError) ``` Raised when a template cannot be read or is not a valid template. The message names the offending field by its path in the document, such as ``tables[2].cells[0].datapoint``. Examples: >>> load_template({"format": "dpm-synth-template/1", "module": "M"}) Traceback (most recent call last): ... dpmsynth.template.TemplateError: release: required ## `dpmsynth.TEMPLATE_FORMAT` ```python TEMPLATE_FORMAT = 'dpm-synth-template/1' ``` The ``format`` value of the templates this version reads. ## Generating ## `dpmsynth.generate` ```python def generate(template: ModuleTemplate, *, seed: int = 0, open_rows: int = 2, max_rounds: int = 40, entity: str = DEFAULT_ENTITY, reporting_level: str = 'CON', reference_date: date | None = None) -> GenerationResult ``` Generate a report for ``template`` that satisfies as many rules as it can. Args: template: Module template, from `load_template`. seed: Random seed; the same seed, template, options and versions give the same report. open_rows: Key rows per open table, from 1 to ``MAX_OPEN_ROWS`` (1,000). Enumerated keys take their allowed items in order, so a table whose keys allow fewer combinations gets fewer rows. max_rounds: Upper bound on evaluate-and-repair rounds; 0 evaluates the initial values only. entity: LEI of the reporting entity. reporting_level: Suffix of the entity identifier, such as ``CON`` or ``IND``; also the ``ReportingLevel`` parameter, in lower case. reference_date: Overrides the template's reference date. Returns: GenerationResult: The report and the final rule status. Raises: ValueError: If ``open_rows`` or ``max_rounds`` is out of range, or ``entity`` or ``reporting_level`` is not an identifier. Examples: >>> from dpmsynth import load_template >>> result = generate(load_template("minimal.json"), seed=7) >>> result.passed, result.failed (2, []) >>> result.report.entity_id 'SYNTHETICDPMXL000002.CON' ## `dpmsynth.GenerationResult` ```python class GenerationResult(report: Report, data: SyntheticData, rules: int, passed: int, failed: list[str], not_evaluated: dict[str, str], not_applicable: list[str], rounds: int, repairs: int, history: list[int] = list()) ``` The generated report and how far the repair loop got. ``passed``, ``failed``, ``not_evaluated`` and ``not_applicable`` together count every rule of the template once. Attributes: report: The report, ready for ``dpmxbrl.write_xbrl_csv``. data: The generated values, a dpm-xl data provider; pass it to `write_scaled_package` or evaluate rules on it. rules: Rules in the template. passed: Rules that pass on the final, rounded values. failed: Codes of the rules that still fail. not_evaluated: Rules dpm-xl could not evaluate, such as ``time_shift`` without a previous report or a syntax error, with the reason, by rule code. not_applicable: Codes of the rules whose precondition is false or that select no cell of the template. rounds: Evaluate-and-repair rounds run. repairs: Failing records repaired over all rounds. history: Failing rules at the start of each round. ## `dpmsynth.SyntheticData` ```python class SyntheticData(template: ModuleTemplate, rng: random.Random, open_rows: int, *, entity: str = DEFAULT_ENTITY, reporting_level: str = 'CON') ``` Generated values for a template, readable as a dpm-xl data provider. `generate` creates and repairs one; it is `GenerationResult.data`. It implements the ``dpmxl.DataProvider`` protocol, with the optional ``open_keys``, so ``dpmxl.evaluate_rule(expression, data)`` evaluates any rule on the generated values. Attributes: template: The template, with the reference date the report uses. values: Cell values by ``(datapoint, keys)``, where ``keys`` is a tuple of ``(key code, value)`` pairs of an open table's row, and ``()`` for a closed table. Numbers are ``dpmxl.Number``, enumerated values ``dpmxl.Item``. key_rows: Key rows by table code; ``[()]`` for a closed table. entity: The LEI of the reporting entity. reporting_level: ``CON`` or ``IND``, the suffix of the entity identifier and the ``ReportingLevel`` parameter. decimals: Reporting decimals by data type code. ## `dpmsynth.DEFAULT_ENTITY` ```python DEFAULT_ENTITY = 'SYNTHETICDPMXL000002' ``` The fictional LEI that reports are generated for unless told otherwise. ## `dpmsynth.DEFAULT_REFERENCE_DATE` ```python DEFAULT_REFERENCE_DATE = date(2026, 12, 31) ``` The reference date of a template that gives none. ## `dpmsynth.LEI_PREFIX` ```python LEI_PREFIX = 'SYNTH' ``` The prefix of every LEI-shaped identifier dpm-synth generates. Generated identifiers are ``SYNTH`` followed by 13 letters and digits and two ISO 17442 check digits, so they pass LEI validation but are visibly fictional and cannot coincide with an entity's real LEI by chance. ## `dpmsynth.MAX_OPEN_ROWS` ```python MAX_OPEN_ROWS = 1000 ``` The most key rows per open table that ``generate`` solves. Every rule is evaluated on every key row in every round, so the time grows with the rows; for larger tables, write a scaled package instead. ## Writing ## `dpmsynth.build_report` ```python def build_report(data: SyntheticData) -> Report ``` Turn generated values into a report that dpm-xbrl can write. Facts follow the EBA xBRL-CSV convention: a ``datapoint`` and ``factValue`` per cell, with the open keys of the row. The entity is ``.`` in the reporting-subject scheme, every table's filing indicator is reported, and a ``decimals*`` parameter is set for each numeric data type the template uses. `generate` calls it; call it yourself after changing ``data.values``. Args: data: Generated values, ``GenerationResult.data``. Returns: Report: The report, for ``dpmxbrl.write_xbrl_csv``. Examples: >>> from dpmsynth import generate, load_template >>> data = generate(load_template("minimal.json"), seed=7).data >>> report = build_report(data) >>> [fact.datapoint for fact in report.facts] ['dp1001', 'dp1002', 'dp1003'] ## `dpmsynth.write_scaled_package` ```python def write_scaled_package(data: SyntheticData, target: str | os.PathLike[str], *, rows: int | Mapping[str, int]) -> dict[str, int] ``` Write an xBRL-CSV package with every open table scaled to ``rows`` key rows. Closed tables are written as generated. Each open table gets ``rows`` key rows, whose values repeat the generated rows in turn. Typed keys get new identifiers; enumerated keys keep the values of the generated row that is repeated, so rules that filter on them still hold, and a table with only enumerated keys keeps its generated rows. The package is laid out, named and described exactly as ``dpmxbrl.write_xbrl_csv`` writes the unscaled report, but tables are streamed to the ZIP file, so memory stays flat. Args: data: Generated values, ``GenerationResult.data``. target: ZIP file to write; its name without ``.zip`` names the package's root folder. rows: Key rows per open table, or a mapping of table code to rows; tables missing from the mapping keep their generated rows. Returns: dict[str, int]: Fact rows written per table code. Raises: ValueError: If a row count is not a whole number of 1 or more. dpmxbrl.ReportError: If dpm-xbrl cannot write the package header. OSError: If the file cannot be written. Examples: >>> from dpmsynth import generate, load_template >>> result = generate(load_template("sample.json"), seed=7) >>> written = write_scaled_package(result.data, "big.zip", rows=1000) >>> written["S_03.00"], written["S_02.00.a"] (1000, 4) ## Version ## `dpmsynth.__version__` ```python __version__ = version('dpm-synth') ``` [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Using dpm-synth with AI agents This page is for coding agents that write code *using* dpm-synth, and for people who brief them. It gives the essential API, the contracts to rely on and the mistakes to avoid. Agents working *on* the dpm-synth repository should read `AGENTS.md` in its root instead. Machine-readable copies of these docs: - [`llms.txt`](llms.txt): an index of every page with a one-line description ([llmstxt.org](https://llmstxt.org) format). - [`llms-full.txt`](llms-full.txt): every page in one Markdown file, including the API reference. - Each page as Markdown: replace the trailing `/` of a page URL with `.md`, for example `https://luukhopman.github.io/dpm-synth/guides/load-testing.md` (section index pages are `.../guides/index.md`). The sibling packages have the same files: [dpm-xl](https://luukhopman.github.io/dpm-xl/llms.txt) and [dpm-xbrl](https://luukhopman.github.io/dpm-xbrl/llms.txt). ## Essentials ```python from dpmxbrl import write_xbrl_csv from dpmxl import evaluate_rule from dpmsynth import TemplateError, generate, load_template, write_scaled_package try: template = load_template("sample.json") # a path, or the parsed JSON as a dict except TemplateError as error: # the only exception for a bad template raise SystemExit(f"bad template: {error}") result = generate(template, seed=7) # same inputs and versions, same report result.passed # 8: rules that pass on the rounded values result.failed # []: codes of rules that still fail result.not_evaluated # {"history": "...time_shift..."}: dpm-xl could not evaluate result.not_applicable # []: precondition false, or no cell selected result.report # dpmxbrl.Report, ready to write result.data # SyntheticData: a dpm-xl provider over the values write_xbrl_csv(result.report, "report.zip") write_scaled_package(result.data, "big.zip", rows=1000) # load testing evaluate_rule("{tS_01.00, r0010, c0010} >= 0", result.data).status ``` | Task | Call | | --- | --- | | Load a template | `load_template(path_or_mapping) -> ModuleTemplate`; raises `TemplateError` | | Generate | `generate(template, *, seed=0, open_rows=2, max_rounds=40, entity="SYNTHETICDPMXL000002", reporting_level="CON", reference_date=None) -> GenerationResult` | | Write the package | `dpmxbrl.write_xbrl_csv(result.report, "report.zip")` | | Millions of rows | `write_scaled_package(result.data, "big.zip", rows=N or {table: N}) -> dict[str, int]` | | Rebuild the report after changing values | `build_report(result.data) -> dpmxbrl.Report` | | Evaluate a rule on the values | `dpmxl.evaluate_rule(expression, result.data, precondition=...)` | | Keep only some rules | `dataclasses.replace(template, rules=tuple(...))` | | Command line | `dpm-synth generate TEMPLATE -o OUT.zip [--seed N] [--json]`; exit 0 all pass, 1 some fail, 2 usage, 3 bad input | ## Contracts - `load_template` raises `TemplateError` (a `ValueError`) for every unreadable, non-JSON or invalid template, naming the field by its path, such as `tables[2].cells[0].datapoint`. Unknown fields are ignored. - `generate` raises `ValueError` only for invalid options. Rules that do not parse or cannot be evaluated never raise: they are in `not_evaluated`. - `passed + len(failed) + len(not_evaluated) + len(not_applicable) == rules`. - The same template, seed, options, output file name and versions give the same bytes. - `ModuleTemplate` and its parts are frozen dataclasses; `GenerationResult` is frozen too, but `result.data.values` is a mutable dict. - `--json` output carries `schema_version` (1); it changes only with a major version. - Every generated LEI-shaped identifier starts with `SYNTH` (`LEI_PREFIX`) and has valid check digits; the default entity is the fictional `SYNTHETICDPMXL000002`. None can be a real entity's LEI. - Nothing is fetched over the network. ## Pitfalls - **Not every rule passes.** Real modules keep some failing rules; read `result.failed`, never assume it is empty. See [limits of the loop](concepts/limits.md). - **Exit status 1 is not a crash.** The package is written; compare the JSON `failed` list with what you expect. - **Scaled rows are not evaluated.** Rules that sum an open table fail on a scaled package; use the unscaled one to test validation results. - **The entity and reporting level live in the data.** Pass `entity`, `reporting_level` and `reference_date` to `generate`; `build_report` and `write_scaled_package` take them from `result.data`. - **The root folder is the file name.** Packages are named as dpm-xbrl names them: `report.zip` holds `report/...`. The EBA file-naming filing rule is a warning on synthetic names. - **Rule order matters for reproducibility.** Reordering the template's tables, cells or rules can change the values for a seed. - **Values are synthetic.** They satisfy rules, not economics; do not use them as realistic magnitudes. ## Where to look next - [Getting started](getting-started.md) for a guided first use. - [Template format](reference/template-format.md) to write or check a template. - [Python API](reference/api.md) for every signature, and [CLI](reference/cli.md), [exit statuses](reference/exit-statuses.md) and [JSON output](reference/json-output.md). [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Changelog All notable changes to this project are documented here. The project follows [semantic versioning](https://semver.org). ## 1.0.0 - 2026-10-24 The first public release. ### Generating reports - `generate` fills every cell of a `dpm-synth-template/1` template with a value for its data type and repairs the values, round by round, until the module's DPM-XL rules pass. Repairs cover sums, differences, products of two cells, `abs`, sign rules (also inside `and`), `isnull`, `in` with any set operand, `match` for LEIs and `dd/mm/yyyy` dates, `and`, `or`, `not` and `if`. Values are rounded to the reporting decimals and every rule is evaluated once more. - `GenerationResult` holds the report, ready for `dpmxbrl.write_xbrl_csv`, the generated values (`data`, a dpm-xl data provider) and every rule counted once: `passed`, `failed`, `not_evaluated` with the reason, and `not_applicable`. - Reports are reproducible: the same template, seed, options and versions give the same bytes. - Reports are synthetic: the entity is the fictional LEI `SYNTHETICDPMXL000002` (`DEFAULT_ENTITY`), and every generated LEI-shaped identifier starts with `SYNTH` (`LEI_PREFIX`) and has valid ISO 17442 check digits, so none can be a real entity's LEI. - `build_report` turns changed values into a report again. - `write_scaled_package` streams open tables with millions of rows into a package laid out as dpm-xbrl writes it, for load tests; scaled rows keep the enumerated key values of the solved rows they repeat. ### Templates - `load_template` reads and validates a template field by field, and raises `TemplateError`, naming the field such as `tables[2].cells[0].datapoint`, for an unreadable, non-JSON or invalid template, including repeated table, cell or rule codes and text that cannot be written as UTF-8. Unknown fields are ignored. ### Command line - `dpm-synth generate` with `--seed`, `--open-rows`, `--max-rounds`, `--scale-rows`, `--reference-date`, `--entity`, `--level` and `--json` (with `schema_version` 1), `--version`, and examples in `--help`. - Exit statuses: 0 every evaluable rule passes, 1 some fail (the package is written), 2 usage error, 3 unreadable or invalid template, or a package that cannot be written. ### Project - Depends on dpm-xl and dpm-xbrl 1.x, and uses only their public APIs. - Python 3.11 to 3.15, tested in CI. - A documentation site at https://luukhopman.github.io/dpm-synth/, with `llms.txt` and `llms-full.txt` for AI agents. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Stability and versioning dpm-synth follows [semantic versioning][semver]. A major version may break the public API; minor and patch versions do not. The [changelog](changelog.md) lists every change, with breaking changes marked. ## The public API - The names in `dpmsynth.__all__`, documented in the [Python API](../reference/api.md), with the fields of `ModuleTemplate`, `TemplateTable`, `OpenKey`, `TemplateCell`, `TemplateRule` and `GenerationResult`, and the attributes of `SyntheticData` documented there. - The `TemplateError` contract: every unreadable or invalid template raises `TemplateError`, a `ValueError`. - The [`dpm-synth-template/1`](../reference/template-format.md) format. A change that a reader of version 1 cannot ignore gets a new format version; dpm-synth keeps reading version 1 for the rest of the major version. - The CLI: its commands, options, [exit statuses](../reference/exit-statuses.md) and the [`--json` layout](../reference/json-output.md), versioned by `schema_version`. Modules and names that start with an underscore are internal, as are names not in `dpmsynth.__all__`; import public names from `dpmsynth`. `dpmsynth.__version__` gives the installed version. ## What may change in a minor release - New functions, parameters with defaults, fields, CLI options, JSON fields and repairs. - **The values generated for a seed.** A better repair, or a fix in dpm-xl or dpm-xbrl, can change the values and which rules pass. The changelog notes it. Pin all three packages if a fixture must stay byte for byte the same; see [reproduce a report](../guides/reproducible-seeds.md). - The wording of the text summary, error messages and `not_evaluated` reasons. - Performance characteristics. ## Python versions dpm-synth supports Python 3.11 to 3.15. Support for a Python version is dropped only in a minor release, after its end of life. ## Dependencies dpm-synth depends on dpm-xl and dpm-xbrl, within one major version of each (`>=1.0,<2`), and uses only their public APIs. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Security policy dpm-synth reads templates and writes report packages. Templates are usually exported by your own tools, but they may come from elsewhere, so bugs in reading them can matter. ## Supported versions Security fixes are released for the latest minor version of the latest major version. ## Reporting a vulnerability Please report vulnerabilities privately, not in a public issue: use [GitHub's private vulnerability reporting](https://github.com/luukhopman/dpm-synth/security/advisories/new) for the repository, or e-mail github@luukhopman.nl. Include the dpm-synth, dpm-xl, dpm-xbrl and Python versions, and, if you can, a small template that shows the problem. The fix is released as a patch version and described in the changelog. ## What counts - Loading a template raises anything other than `TemplateError`, or the command line prints a traceback, for any template file. - Generating writes files other than the output package, makes a network request, or runs code from the template. Rules are DPM-XL, evaluated by dpm-xl, never Python. - A written package lets a reader escape its root folder, for example through a table code; table codes are restricted to safe file names. Generation time grows with the size of a template and its rules, and scaled packages are as large as asked for; bound `--scale-rows` and the template size when they come from untrusted sources. Problems in reading or writing packages, or in evaluating rules, belong to [dpm-xbrl](https://luukhopman.github.io/dpm-xbrl/project/security/) and [dpm-xl](https://luukhopman.github.io/dpm-xl/project/security/). [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Contributing Bug reports, questions and pull requests are welcome on [GitHub](https://github.com/luukhopman/dpm-synth/issues). For a larger change, such as a new repair or a solver, open an issue first to agree on the approach. ## Set up dpm-synth uses [uv](https://docs.astral.sh/uv/): ```text git clone https://github.com/luukhopman/dpm-synth cd dpm-synth uv sync ``` ## Check a change ```text uv run pytest # tests, including every docs example uv run ruff check uv run ruff format --check uv run ty check uv run python scripts/build_docs.py # strict docs build into site/ ``` `uv run zensical serve` previews the documentation while you edit it. ## Conventions - Every behaviour has a test. Build templates in code with `tests/support.py`, or add a small one to `docs/samples`: every sample is generated, written, read back with dpm-xbrl and evaluated with dpm-xl by `tests/test_end_to_end.py`. - Public functions and classes have Google-style docstrings; they are the API reference. - dpm-synth depends on dpm-xl and dpm-xbrl only, and uses only the names in their `__all__`. New runtime dependencies need a good reason. - Keep `__all__`, the docs and the [changelog](changelog.md) in step with the code. Examples in the docs are run by `tests/test_docs.py`, so show real output. - A change to the generated values for a seed is noted in the changelog. - Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/), such as `fix(generator): keep bounds when repairing products`. `AGENTS.md` in the repository holds the same information for coding agents. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org # Licence dpm-synth is released under the MIT licence: ```text MIT License Copyright (c) 2026 Luuk Hopman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` dpm-synth is an independent project, not affiliated with or endorsed by the European Banking Authority. "EBA" and the names of EBA reporting frameworks and modules are used only to describe the reports dpm-synth generates. The reports are synthetic: their entity is a fictional LEI and their values are generated. [dpm-xl]: https://luukhopman.github.io/dpm-xl/ [dpm-xbrl]: https://luukhopman.github.io/dpm-xbrl/ [dpm-xl-provider]: https://luukhopman.github.io/dpm-xl/guides/provider/ [dpm-xbrl-write]: https://luukhopman.github.io/dpm-xbrl/guides/convert-formats/ [dpm-xbrl-filing]: https://luukhopman.github.io/dpm-xbrl/guides/check-filing-rules/ [xbrl-csv-spec]: https://www.xbrl.org/Specification/xbrl-csv/REC-2021-10-13/xbrl-csv-REC-2021-10-13.html [lei-standard]: https://www.iso.org/standard/78829.html [semver]: https://semver.org