<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Regression tests
Step1:  Resetting one generator should not reset others of the same type
Let's create two generators of the same type.
Step2:  The random generators which produce the attributes of items generated by g1 and g2 must be independent of each other. This regression test checks this by making sure that g1.reset() does not interfere with g2.reset(). In particular, the two outputs below should be identical (because the final call g1.reset(12345) should not interfere with the previous call g2.reset(9999)).
Step3:  Sub-generators of custom generator should be initialised with separate seeds
Each custom generator can be re-initialised using a given seed. Internally, this re-initialises each of the constituent generators. This test checks that different seeds are used for each of the individual generators, even if they are of the same type (otherwise the values produced for each attribute would be the same). In particular, the sequences produced for a1, a2, a3 below should be different (and similarly for (b1, b2, b3).
Step4:  NumpyRandomGenerator
This is a quick sanity check that resetting a NumpyRandomGenerator really fully resets the state and leads to independent runs.
Step5:  Zip should use different seeds internally to reset its input generators
Step6:  If .reset() is called on a Zip generator, it should use different seeds internally to reset each of its constituent input generators. In particular, the output of h1 and h2 below should be different.
Step7:  Multiple FakerGenerator instances should be independent
<ASSISTANT_TASK:>
Python Code:
import tohu
from tohu import *
from utils import print_generated_sequence
print(f"Tohu version: {tohu.__version__}")
Explanation: Regression tests
End of explanation
class FoobarGenerator(CustomGenerator):
    a = Integer(low=1000, high=3000)
    b = Sequential(prefix="Foo_", digits=2)
    c = Float(low=1.0, high=4.0)
g1 = FoobarGenerator()
g2 = FoobarGenerator()
Explanation: Resetting one generator should not reset others of the same type
Let's create two generators of the same type.
End of explanation
g1.reset(seed=12345)
g2.reset(seed=9999)
print_generated_sequence(g1, num=3, sep='\n')
print_generated_sequence(g2, num=3, sep='\n')
print("-------------------------------------------------")
g1.reset(seed=12345)
g2.reset(seed=9999)
g1.reset(seed=12345)
print_generated_sequence(g1, num=3, sep='\n')
print_generated_sequence(g2, num=3, sep='\n')
Explanation: The random generators which produce the attributes of items generated by g1 and g2 must be independent of each other. This regression test checks this by making sure that g1.reset() does not interfere with g2.reset(). In particular, the two outputs below should be identical (because the final call g1.reset(12345) should not interfere with the previous call g2.reset(9999)).
End of explanation
class FoobarGenerator(CustomGenerator):
    a1 = HashDigest(length=8)
    a2 = HashDigest(length=8)
    a3 = HashDigest(length=8)
    
    b1 = Integer(low=0, high=1000)
    b2 = Integer(low=0, high=1000)
    b3 = Integer(low=0, high=1000)
g = FoobarGenerator()
g.reset(seed=99999)
print_generated_sequence(g, num=10, sep='\n')
Explanation: Sub-generators of custom generator should be initialised with separate seeds
Each custom generator can be re-initialised using a given seed. Internally, this re-initialises each of the constituent generators. This test checks that different seeds are used for each of the individual generators, even if they are of the same type (otherwise the values produced for each attribute would be the same). In particular, the sequences produced for a1, a2, a3 below should be different (and similarly for (b1, b2, b3).
End of explanation
g = NumpyRandomGenerator(method="normal", loc=3.0, scale=5.0)
g.reset(seed=12345); print_generated_sequence(g, num=4)
g.reset(seed=99999); print_generated_sequence(g, num=4)
g1 = NumpyRandomGenerator(method="normal", loc=3.0, scale=5.0)
g2 = NumpyRandomGenerator(method="normal", loc=3.0, scale=5.0)
g1.reset(seed=12345)
g2.reset(seed=99999)
print_generated_sequence(g1, num=4)
print_generated_sequence(g2, num=4)
Explanation: NumpyRandomGenerator
This is a quick sanity check that resetting a NumpyRandomGenerator really fully resets the state and leads to independent runs.
End of explanation
g1 = Integer(0, 100)
g2 = Integer(0, 100)
g1.reset(12345)
g2.reset(12345)
print_generated_sequence(g1, num=10)
print_generated_sequence(g2, num=10)
z = Zip(g1, g2)
h1, h2 = Split(z)
Explanation: Zip should use different seeds internally to reset its input generators
End of explanation
z.reset(seed=12345)
print_generated_sequence(h1, num=10)
print_generated_sequence(h2, num=10)
Explanation: If .reset() is called on a Zip generator, it should use different seeds internally to reset each of its constituent input generators. In particular, the output of h1 and h2 below should be different.
End of explanation
g1 = FakerGenerator(method="name")
g2 = FakerGenerator(method="name")
g1.reset(seed=12345)
g2.reset(seed=12345)
print_generated_sequence(g1, num=5)
print_generated_sequence(g2, num=5)
Explanation: Multiple FakerGenerator instances should be independent
End of explanation
<END_TASK>