Skip to content

Commit 15b566c

Browse files
authored
Merge pull request #115 from CitrineInformatics/bugfix/cake-errors
Bugfixes for cake demo
2 parents 31f2000 + 9cac5e5 commit 15b566c

File tree

7 files changed

+135
-146
lines changed

7 files changed

+135
-146
lines changed

gemd/demo/cake.py

Lines changed: 37 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def make_cake_templates():
222222
)
223223

224224
for key in tmpl:
225-
tmpl[key].add_uid(TEMPLATE_SCOPE, key)
225+
tmpl[key].add_uid(TEMPLATE_SCOPE, key.lower().replace(' ', '-'))
226226
return tmpl
227227

228228

@@ -233,21 +233,30 @@ def make_cake_spec(tmpl=None):
233233
if tmpl is None:
234234
tmpl = make_cake_templates()
235235

236-
def _make_ingredient(material, **kwargs):
236+
def _make_ingredient(*, material, process, **kwargs):
237237
"""Convenience method to utilize material fields in creating an ingredient's arguments."""
238238
return IngredientSpec(
239239
name=material.name.lower(),
240240
tags=list(material.tags),
241241
material=material,
242+
process=process,
243+
uids={DEMO_SCOPE: "{}--{}".format(material.uids[DEMO_SCOPE],
244+
process.uids[DEMO_SCOPE]
245+
)},
242246
**kwargs
243247
)
244248

245-
def _make_material(material_name, process_tmpl_name, process_kwargs, **material_kwargs):
249+
def _make_material(*, material_name, template, process_tmpl_name, process_kwargs,
250+
**material_kwargs):
246251
"""Convenience method to reuse material name in creating a material's arguments."""
252+
process_name = "{} {}".format(process_tmpl_name, material_name)
247253
return MaterialSpec(
248254
name=material_name,
255+
uids={DEMO_SCOPE: material_name.lower().replace(' ', '-')},
256+
template=template,
249257
process=ProcessSpec(
250-
name="{} {}".format(process_tmpl_name, material_name),
258+
name=process_name,
259+
uids={DEMO_SCOPE: process_name.lower().replace(' ', '-')},
251260
template=tmpl[process_tmpl_name],
252261
**process_kwargs
253262
),
@@ -624,6 +633,7 @@ def _make_material(material_name, process_tmpl_name, process_kwargs, **material_
624633
_make_ingredient(
625634
material=eggs,
626635
labels=['wet'],
636+
process=wetmix.process,
627637
absolute_quantity=NominalReal(nominal=4, units='')
628638
)
629639

@@ -741,25 +751,6 @@ def _make_material(material_name, process_tmpl_name, process_kwargs, **material_
741751
mass_fraction=NominalReal(nominal=0.6387, units='') # 4 c @ 30 g/ 0.25 cups
742752
)
743753

744-
# Crawl tree and annotate with uids; only add ids if there's nothing there
745-
def _make_fuzzer():
746-
"""Generate closure that knows if it's seen a given name."""
747-
seen = set()
748-
749-
def _fuzz_func(obj):
750-
"""Add fuzz to name in ID as necessary."""
751-
name = 'ing-' if isinstance(obj, IngredientSpec) else ''
752-
name += obj.name
753-
while name.lower() in seen:
754-
name += '-again'
755-
seen.add(name.lower())
756-
return name
757-
758-
return _fuzz_func
759-
760-
_name_fuzz = _make_fuzzer()
761-
recursive_foreach(cake, lambda obj: obj.uids or obj.add_uid(DEMO_SCOPE, _name_fuzz(obj)))
762-
763754
return cake
764755

765756

@@ -770,6 +761,13 @@ def make_cake(seed=None, tmpl=None, cake_spec=None, toothpick_img=None):
770761

771762
if seed is not None:
772763
random.seed(seed)
764+
# Code to generate quasi-repeatable run annotations
765+
# Note there are potential machine dependencies
766+
md5 = hashlib.md5()
767+
for x in random.getstate()[1]:
768+
md5.update(struct.pack(">I", x))
769+
run_key = md5.hexdigest()
770+
773771
######################################################################
774772
# Parent Objects
775773
if tmpl is None:
@@ -785,10 +783,13 @@ def make_cake(seed=None, tmpl=None, cake_spec=None, toothpick_img=None):
785783
drygoods = ['Acme', 'A1', 'Reliable', "Big Box"]
786784
cake.process.source = PerformedSource(performed_by=random.choice(operators),
787785
performed_date='2015-03-14')
788-
# Replace Abstract/In General
789-
queue = [cake]
790-
while queue:
791-
item = queue.pop(0)
786+
787+
def _randomize_object(item):
788+
# Add in the randomized particular values
789+
if not isinstance(item, (MaterialRun, ProcessRun, IngredientRun)):
790+
return
791+
792+
item.add_uid(DEMO_SCOPE, '{}-{}'.format(item.spec.uids[DEMO_SCOPE], run_key))
792793
if item.spec.tags is not None:
793794
item.tags = list(item.spec.tags)
794795
if item.spec.notes: # Neither None or empty string
@@ -800,17 +801,14 @@ def make_cake(seed=None, tmpl=None, cake_spec=None, toothpick_img=None):
800801
else:
801802
supplier = random.choice(drygoods)
802803
item.name = "{} {}".format(supplier, item.spec.name)
803-
queue.append(item.process)
804-
elif isinstance(item, ProcessRun):
805-
queue.extend(item.ingredients)
804+
if isinstance(item, ProcessRun):
806805
if item.template.name == "Procuring":
807806
item.source = PerformedSource(performed_by='hamilton',
808807
performed_date='2015-02-17')
809808
item.name = "{} {}".format(item.template.name, item.output_material.name)
810809
else:
811810
item.source = cake.process.source
812-
elif isinstance(item, IngredientRun):
813-
queue.append(item.material)
811+
if isinstance(item, IngredientRun):
814812
fuzz = 0.95 + 0.1 * random.random()
815813
if item.spec.absolute_quantity is not None:
816814
item.absolute_quantity = \
@@ -833,9 +831,7 @@ def make_cake(seed=None, tmpl=None, cake_spec=None, toothpick_img=None):
833831
NormalReal(mean=fuzz * item.spec.number_fraction.nominal,
834832
std=0.05 * item.spec.number_fraction.nominal,
835833
units=item.spec.number_fraction.units)
836-
837-
else:
838-
raise TypeError("Unexpected object in the queue")
834+
recursive_foreach(cake, _randomize_object)
839835

840836
frosting = \
841837
next(x.material for x in cake.process.ingredients if 'rosting' in x.name)
@@ -882,9 +878,14 @@ def _find_name(name, material):
882878
)
883879
sugar_content.spec = salt_content.spec
884880

881+
# Note that while specs are regenerated each make_cake invocation, they are all identical
885882
for msr in (cake_taste, cake_appearance, frosting_taste, frosting_sweetness,
886883
baked_doneness, flour_content, salt_content, sugar_content):
887-
msr.spec.add_uid(DEMO_SCOPE, msr.spec.name)
884+
msr.spec.add_uid(DEMO_SCOPE, msr.spec.name.lower())
885+
msr.add_uid(DEMO_SCOPE, '{}--{}-{}'.format(msr.spec.uids[DEMO_SCOPE],
886+
msr.material.spec.uids[DEMO_SCOPE],
887+
run_key
888+
))
888889

889890
######################################################################
890891
# Let's add some attributes
@@ -1029,35 +1030,6 @@ def _find_name(name, material):
10291030
origin="specified"
10301031
))
10311032

1032-
# Code to generate quasi-repeatable run annotations
1033-
# Note there are potential machine dependencies
1034-
md5 = hashlib.md5()
1035-
for x in random.getstate()[1]:
1036-
md5.update(struct.pack(">I", x))
1037-
run_key = md5.hexdigest()
1038-
1039-
# Crawl tree and annotate with uids; only add ids if there's nothing there
1040-
def _make_disambiguator():
1041-
"""Generate a closure to post-annotate for disambiguation."""
1042-
count = dict()
1043-
1044-
def _disambiguator(name):
1045-
"""Add a number to the name if you've seen it more than once."""
1046-
if name in count:
1047-
count[name] = count[name] + 1
1048-
return "{}-{}".format(name, count[name])
1049-
else:
1050-
count[name] = 1
1051-
return name
1052-
1053-
return _disambiguator
1054-
1055-
_disambig = _make_disambiguator()
1056-
recursive_foreach(
1057-
cake,
1058-
lambda obj: obj.uids or obj.add_uid(DEMO_SCOPE, _disambig(obj.name) + run_key)
1059-
)
1060-
10611033
cake.notes = cake.notes + "; Très délicieux! 😀"
10621034
cake.file_links = [FileLink(
10631035
filename="Photo",

gemd/demo/tests/test_cake.py

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,16 @@
99
from gemd.entity.object.ingredient_run import IngredientRun
1010
from gemd.entity.file_link import FileLink
1111

12-
from gemd.json import dumps, loads
12+
from gemd.json import dumps
1313
from gemd.demo.cake import make_cake_templates, make_cake_spec, make_cake, \
1414
import_toothpick_picture, change_scope
1515
from gemd.util import recursive_foreach
16-
from gemd.entity.util import complete_material_history
1716

1817

1918
def test_cake():
2019
"""Create cake, serialize, deserialize."""
2120
cake = make_cake(seed=42)
2221

23-
def test_for_loss(obj):
24-
assert(obj == loads(dumps(obj)))
25-
recursive_foreach(cake, test_for_loss)
26-
27-
# And verify equality was working in the first place
28-
cake2 = loads(dumps(cake))
29-
cake2.name = "It's a trap!"
30-
assert(cake2 != cake)
31-
cake2.name = cake.name
32-
assert(cake == cake2)
33-
cake2.uids['new'] = "It's a trap!"
34-
assert(cake2 != cake)
35-
3622
# Check that all the objects show up
3723
tot_count = 0
3824

@@ -41,79 +27,44 @@ def increment(dummy):
4127
tot_count += 1
4228

4329
recursive_foreach(cake, increment)
44-
assert tot_count == 133
45-
46-
# And make sure nothing was lost
47-
tot_count = 0
48-
recursive_foreach(loads(dumps(complete_material_history(cake))), increment)
49-
assert tot_count == 133
30+
assert tot_count == 139
5031

5132
# Check that no UIDs collide
5233
uid_seen = dict()
5334

54-
def check_ids(obj):
35+
def _check_ids(obj):
5536
nonlocal uid_seen
5637
for scope in obj.uids:
57-
lbl = '{}::{}'.format(scope, obj.uids[scope])
38+
lbl = '{}::{}'.format(scope, obj.uids[scope].lower())
5839
if lbl in uid_seen:
5940
assert uid_seen[lbl] == id(obj), "'{}' seen twice".format(lbl)
6041
uid_seen[lbl] = id(obj)
61-
recursive_foreach(cake, check_ids)
62-
63-
queue = [cake]
64-
seen = set()
65-
while queue:
66-
obj = queue.pop()
67-
if obj in seen:
68-
continue
69-
70-
seen.add(obj)
42+
recursive_foreach(cake, _check_ids)
7143

44+
# Check that all recursive and square links are structured correctly
45+
def _check_crosslinks(obj):
7246
if isinstance(obj, MaterialSpec):
73-
if obj.process is not None:
74-
queue.append(obj.process)
75-
assert obj.process.output_material == obj
47+
assert obj.process.output_material == obj
7648
elif isinstance(obj, MaterialRun):
77-
if obj.process is not None:
78-
queue.append(obj.process)
79-
assert obj.process.output_material == obj
80-
if obj.measurements:
81-
queue.extend(obj.measurements)
82-
for msr in obj.measurements:
83-
assert msr.material == obj
84-
if obj.spec is not None:
85-
queue.append(obj.spec)
86-
if obj.process is not None:
87-
assert obj.spec.process == obj.process.spec
49+
assert obj.process.output_material == obj
50+
for msr in obj.measurements:
51+
assert msr.material == obj
52+
assert obj.spec.process == obj.process.spec
8853
elif isinstance(obj, ProcessRun):
89-
if obj.ingredients:
90-
queue.extend(obj.ingredients)
91-
if obj.output_material is not None:
92-
queue.append(obj.output_material)
93-
assert obj.output_material.process == obj
94-
if obj.spec is not None:
95-
assert obj.spec.output_material == obj.output_material.spec
54+
assert obj.output_material.process == obj
55+
assert obj.spec.output_material == obj.output_material.spec
9656
elif isinstance(obj, ProcessSpec):
97-
if obj.ingredients:
98-
queue.extend(obj.ingredients)
99-
if obj.output_material is not None:
100-
queue.append(obj.output_material)
101-
assert obj.output_material.process == obj
57+
assert obj.output_material.process == obj
10258
elif isinstance(obj, MeasurementSpec):
10359
pass # Doesn't link
10460
elif isinstance(obj, MeasurementRun):
105-
if obj.spec:
106-
queue.append(obj.spec)
61+
assert obj in obj.material.measurements
10762
elif isinstance(obj, IngredientSpec):
108-
if obj.material:
109-
queue.append(obj.material)
63+
assert obj in obj.process.ingredients
11064
elif isinstance(obj, IngredientRun):
111-
if obj.spec:
112-
queue.append(obj.spec)
113-
if obj.material and isinstance(obj.material, MaterialRun):
114-
assert obj.spec.material == obj.material.spec
115-
if obj.material:
116-
queue.append(obj.material)
65+
assert obj in obj.process.ingredients
66+
assert obj.spec.material == obj.material.spec
67+
recursive_foreach(cake, _check_crosslinks)
11768

11869

11970
def test_cake_sigs():

gemd/entity/value/nominal_integer.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,20 @@ class NominalInteger(IntegerValue):
1515

1616
typ = "nominal_integer"
1717

18-
def __init__(self, nominal=None):
19-
assert isinstance(nominal, int), \
20-
"nominal value must be an int"
18+
def __init__(self, nominal):
19+
self._nominal = None
2120
self.nominal = nominal
21+
22+
@property
23+
def nominal(self) -> int:
24+
"""A proscribed integer value without uncertainty."""
25+
return int(self._nominal)
26+
27+
@nominal.setter
28+
def nominal(self, nominal: int) -> None:
29+
"""A proscribed integer value without uncertainty."""
30+
# This check/cast is necessary to handle JSON serialization behavior under 3.6
31+
if not isinstance(nominal, (int, float)) or int(nominal) != nominal:
32+
raise TypeError("nominal must be an int; got an {}({})".format(type(nominal), nominal))
33+
34+
self._nominal = int(nominal)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Tests of the UniformInteger class."""
2+
import pytest
3+
4+
from gemd.entity.value.nominal_integer import NominalInteger
5+
6+
7+
def test_bounds_are_integers():
8+
"""Value must be an integer."""
9+
NominalInteger(5)
10+
with pytest.raises(TypeError):
11+
NominalInteger(5.7)
12+
with pytest.raises(TypeError):
13+
NominalInteger("five")

gemd/entity/value/tests/test_uniform_integer.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ def test_bounds_order():
88
"""Lower bound must be <= upper bound."""
99
UniformInteger(3, 7)
1010
UniformInteger(12, 12)
11-
with pytest.raises(AssertionError):
11+
with pytest.raises(ValueError):
1212
UniformInteger(22, 18)
13+
with pytest.raises(ValueError):
14+
UniformInteger(3, 7).lower_bound = 10
15+
with pytest.raises(ValueError):
16+
UniformInteger(3, 7).upper_bound = 1
1317

1418

1519
def test_bounds_are_integers():
1620
"""Lower bound and upper bound must be integers."""
17-
with pytest.raises(AssertionError):
21+
with pytest.raises(TypeError):
1822
UniformInteger(5.7, 10)
19-
with pytest.raises(AssertionError):
23+
with pytest.raises(TypeError):
2024
UniformInteger(1, "five")

0 commit comments

Comments
 (0)