Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add summation feature #777

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ use `context.evaluate_raw()` instead of `context.evaluate()`.

Plugins that require "memory" or "state" are possible using `PluginResult`
objects or subclasses. Consider a plugin that generates child objects
that include values that sum up values on child objects to a value specified on a parent:
that include values that sum up values on child objects to a value specified on a parent (similar to a simple version
of `Math.random_partition`):

```yaml
# examples/sum_child_values.yml
Expand Down
102 changes: 102 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1861,6 +1861,108 @@ Or:
twelve: ${Math.sqrt}
```

#### Rolling up numbers: `Math.random_partition`

Sometimes you want a parent object to have a field value which
is the sum of many child values. Snowfakery allow you to
specify or randomly generate the parent sum value and then
it will generate an appropriate number of children with
values that sum up to match it, using `Math.random_partition`:

```yaml
# examples/math_partition/math_partition_simple.recipe.yml
- plugin: snowfakery.standard_plugins.Math
- object: ParentObject__c
count: 2
fields:
TotalAmount__c:
random_number:
min: 30
max: 90
friends:
- object: ChildObject__c
for_each:
var: child_value
value:
Math.random_partition:
total: ${{ParentObject__c.TotalAmount__c}}
fields:
Amount__c: ${{child_value}}
```

The `Math.random_partition` function splits up a number.
So this recipe might spit out the following
set of parents and children:

```json
ParentObject__c(id=1, TotalAmount__c=40)
ChildObject__c(id=1, Amount__c=3)
ChildObject__c(id=2, Amount__c=1)
ChildObject__c(id=3, Amount__c=24)
ChildObject__c(id=4, Amount__c=12)
ParentObject__c(id=2, TotalAmount__c=83)
ChildObject__c(id=5, Amount__c=2)
ChildObject__c(id=6, Amount__c=81)
```

There are 2 Parent objects created and a random number of
children per parent.

The `Math.random_partition`function takes argument
`min`, which is the smallest
value each part can have, `max`, which is the largest
possible value, `total` which is what all of the values
sum up to and `step` which is a number that each value
must have as a factor. E.g. if `step` is `4` then
values of `4`, `8`, `12` are valid.

For example:

```yaml
# examples/math_partition/sum_simple_example.recipe.yml
- plugin: snowfakery.standard_plugins.Math

- object: Values
for_each:
var: current_value
value:
Math.random_partition:
total: 100
min: 10
max: 50
step: 5
fields:
Amount: ${{current_value}}
```

Which might generate `15,15,25,20,15,10` or `50,50` or `25,50,25`.

If `step` is a number smaller then `1`, then you can generate
pennies for numeric calculations. Valid values are `0.01` (penny
granularity), `0.05` (nickle), `0.10` (dime), `0.25` (quarter) and
`0.50` (half dollars). Other values are not supported.

```yaml
# examples/math_partition/sum_pennies.recipe.yml
- plugin: snowfakery.standard_plugins.Math

- object: Values
for_each:
var: current_value
value:
Math.random_partition:
total: 100
min: 10
max: 50
step: 0.01
fields:
Amount: ${{current_value}}
```

It is possible to specify values which are inconsistent.
When that happens one of the constraints will be
violated.

### Advanced Unique IDs with the UniqueId plugin

There is a plugin which gives you more control over the generation of
Expand Down
17 changes: 17 additions & 0 deletions examples/math_partition/math_partition_simple.recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
- plugin: snowfakery.standard_plugins.Math
- object: ParentObject__c
count: 2
fields:
TotalAmount__c:
random_number:
min: 30
max: 90
friends:
- object: ChildObject__c
for_each:
var: child_value
value:
Math.random_partition:
total: ${{ParentObject__c.TotalAmount__c}}
fields:
Amount__c: ${{child_value}}
13 changes: 13 additions & 0 deletions examples/math_partition/sum_pennies.recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- plugin: snowfakery.standard_plugins.Math

- object: Values
for_each:
var: current_value
value:
Math.random_partition:
total: 100
min: 10
max: 50
step: 0.01
fields:
Amount: ${{current_value}}
15 changes: 15 additions & 0 deletions examples/math_partition/sum_pennies_param.recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
- plugin: snowfakery.standard_plugins.Math
- option: step
default: 0.01

- object: Values
for_each:
var: current_value
value:
Math.random_partition:
total: 100
min: 10
max: 50
step: ${{step}}
fields:
Amount: ${{current_value}}
13 changes: 13 additions & 0 deletions examples/math_partition/sum_simple_example.recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- plugin: snowfakery.standard_plugins.Math

- object: Values
for_each:
var: current_value
value:
Math.random_partition:
total: 100
min: 10
max: 50
step: 5
fields:
Amount: ${{current_value}}
10 changes: 10 additions & 0 deletions examples/math_partition/test_bad_step.recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- plugin: snowfakery.standard_plugins.Math
- object: Obj
for_each:
var: child_value
value:
Math.random_partition:
total: 28
step: 0.3
fields:
Amount: ${{child_value}}
25 changes: 25 additions & 0 deletions examples/sum_plugin_example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This shows how you could create a plugin or feature where
# a parent object generates child objects which sum up
# to any particular value.

- plugin: examples.sum_totals.SummationPlugin
- var: summation_helper
value:
SummationPlugin.summer:
total: 100
step: 10

- object: ParentObject__c
count: 10
fields:
MinimumChildObjectAmount__c: 10
MinimumStep: 5
TotalAmount__c: ${{summation_helper.total}}
friends:
- object: ChildObject__c
count: ${{summation_helper.count}}
fields:
Parent__c:
reference: ParentObject__c
Amount__c: ${{summation_helper.next_amount}}
RunningTotal__c: ${{summation_helper.running_total}}
8 changes: 8 additions & 0 deletions schema/snowfakery_recipe.jsonschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
}
]
},
"for_each": {
"type": "object",
"anyOf": [
{
"$ref": "#/$defs/var"
}
]
},
"fields": {
"type": "object",
"additionalProperties": true
Expand Down
2 changes: 2 additions & 0 deletions snowfakery/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def generate_data(
update_passthrough_fields: T.Sequence[
str
] = (), # pass through these fields from input to output
seed: T.Optional[int] = None,
) -> None:
stopping_criteria = stopping_criteria_from_target_number(target_number)
dburls = dburls or ([dburl] if dburl else [])
Expand Down Expand Up @@ -193,6 +194,7 @@ def open_with_cleanup(file, mode, **kwargs):
plugin_options=plugin_options,
update_input_file=open_update_input_file,
update_passthrough_fields=update_passthrough_fields,
seed=seed,
)

if open_cci_mapping_file:
Expand Down
2 changes: 2 additions & 0 deletions snowfakery/data_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def generate(
plugin_options: dict = None,
update_input_file: OpenFileLike = None,
update_passthrough_fields: T.Sequence[str] = (),
seed: T.Optional[int] = None,
) -> ExecutionSummary:
"""The main entry point to the package for Python applications."""
from .api import SnowfakeryApplication
Expand Down Expand Up @@ -188,6 +189,7 @@ def generate(
parse_result=parse_result,
globals=globls,
continuing=bool(continuation_data),
seed=seed,
) as interpreter:
runtime_context = interpreter.execute()

Expand Down
3 changes: 3 additions & 0 deletions snowfakery/data_generator_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import defaultdict, ChainMap
from datetime import date, datetime, timezone
from contextlib import contextmanager
from random import Random

from typing import Optional, Dict, Sequence, Mapping, NamedTuple, Set
import typing as T
Expand Down Expand Up @@ -300,6 +301,7 @@ def __init__(
snowfakery_plugins: Optional[Mapping[str, callable]] = None,
faker_providers: Sequence[object] = (),
continuing=False,
seed: Optional[int] = None,
):
self.output_stream = output_stream
self.options = options or {}
Expand Down Expand Up @@ -354,6 +356,7 @@ def __init__(
self.globals.nicknames_and_tables,
)
self.resave_objects_from_continuation(globals, self.tables_to_keep_history_for)
self.random_number_generator = Random(seed)

def resave_objects_from_continuation(
self, globals: Globals, tables_to_keep_history_for: T.Iterable[str]
Expand Down
5 changes: 3 additions & 2 deletions snowfakery/plugins.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from random import Random
import sys

from typing import Any, Callable, Mapping, Union, NamedTuple, List, Tuple
Expand Down Expand Up @@ -141,8 +142,8 @@ def current_filename(self):
return self.interpreter.current_context.current_template.filename

@property
def current_filename(self):
return self.interpreter.current_context.current_template.filename
def random_number_generator(self) -> Random:
return self.interpreter.random_number_generator


def lazy(func: Any) -> Callable:
Expand Down
Loading