예제 #1
0
def stirring(duty_cycle=int(config["stirring"][f"duty_cycle{unit}"]),
             duration=None,
             verbose=0):
    def terminate(*args):
        GPIO.cleanup()

    signal.signal(signal.SIGTERM, terminate)
    signal.signal(signal.SIGINT, terminate)

    publish(f"morbidostat/{unit}/{experiment}/log",
            f"[stirring]: start stirring with duty cycle={duty_cycle}",
            verbose=verbose)

    try:
        stirrer = Stirrer(duty_cycle, unit, experiment)
        stirrer.start_stirring()

        if duration is None:
            signal.pause()
        else:
            time.sleep(duration)

    except Exception as e:
        GPIO.cleanup()
        publish(f"morbidostat/{unit}/{experiment}/error_log",
                f"[stirring] failed with {str(e)}",
                verbose=verbose)
        raise e
    finally:
        GPIO.cleanup()
    return
예제 #2
0
    def update_alt_media_fraction(self, media_delta, alt_media_delta):

        total_delta = media_delta + alt_media_delta

        # current mL
        alt_media_ml = VIAL_VOLUME * self.latest_alt_media_fraction
        media_ml = VIAL_VOLUME * (1 - self.latest_alt_media_fraction)

        # remove
        alt_media_ml = alt_media_ml * (1 - total_delta / VIAL_VOLUME)
        media_ml = media_ml * (1 - total_delta / VIAL_VOLUME)

        # add (alt) media
        alt_media_ml = alt_media_ml + alt_media_delta
        media_ml = media_ml + media_delta

        self.latest_alt_media_fraction = alt_media_ml / VIAL_VOLUME

        publish(
            f"morbidostat/{self.unit}/{self.experiment}/{JOB_NAME}/alt_media_fraction",
            self.latest_alt_media_fraction,
            verbose=self.verbose,
            retain=True,
            qos=QOS.EXACTLY_ONCE,
        )

        return self.latest_alt_media_fraction
예제 #3
0
    def set_attr_from_message(self, message):

        new_value = message.payload.decode()
        info_from_topic = split_topic_for_setting(message.topic)
        attr = info_from_topic.attr

        if attr == "$state":
            return self.set_state(new_value)

        if attr not in self.editable_settings:
            return

        assert hasattr(self, attr), f"{self.job_name} has no attr {attr}."
        previous_value = getattr(self, attr)

        try:
            # make sure to cast the input to the same value
            setattr(self, attr, type(previous_value)(new_value))
        except:
            setattr(self, attr, new_value)

        publish(
            f"morbidostat/{self.unit}/{self.experiment}/log",
            f"[{self.job_name}] Updated {attr} from {previous_value} to {getattr(self, attr)}.",
            verbose=self.verbose,
        )
예제 #4
0
    def execute(self, *args, **kwargs) -> events.Event:
        if self.latest_od <= self.min_od:
            return events.NoEvent(f"latest OD less than OD to start diluting, {self.min_od:.2f}")
        else:
            fraction_of_alt_media_to_add = self.pid.update(
                self.latest_growth_rate, dt=self.duration
            )  # duration is measured in minutes, not seconds (as simple_pid would want)

            # dilute more if our OD keeps creeping up - we want to stay in the linear range.
            if self.latest_od > self.max_od:
                publish(
                    f"morbidostat/{self.unit}/{self.experiment}/log",
                    f"[{JOB_NAME}]: executing double dilution since we are above max OD, {self.max_od:.2f}.",
                    verbose=self.verbose,
                )
                volume = 2 * self.volume
            else:
                volume = self.volume

            alt_media_ml = fraction_of_alt_media_to_add * volume
            media_ml = (1 - fraction_of_alt_media_to_add) * volume

            self.execute_io_action(alt_media_ml=alt_media_ml, media_ml=media_ml, waste_ml=volume)
            event = events.AltMediaEvent(
                f"PID output={fraction_of_alt_media_to_add:.2f}, alt_media_ml={alt_media_ml:.2f}mL, media_ml={media_ml:.2f}mL"
            )
            event.media_ml = media_ml  # can be used for testing later
            event.alt_media_ml = alt_media_ml
            return event
예제 #5
0
    def execute_io_action(self, alt_media_ml=0, media_ml=0, waste_ml=0, log=True):
        assert (
            abs(alt_media_ml + media_ml - waste_ml) < 1e-5
        ), f"in order to keep same volume, IO should be equal. {alt_media_ml}, {media_ml}, {waste_ml}"

        if log:
            # TODO: this is not being stored or used.
            publish(
                f"morbidostat/{self.unit}/{self.experiment}/io_batched",
                json.dumps({"alt_media_ml": alt_media_ml, "media_ml": media_ml, "waste_ml": waste_ml}),
                verbose=self.verbose,
            )

        max_ = 0.3
        if (media_ml > max_) or (alt_media_ml > max_):
            if media_ml > max_:
                self.execute_io_action(alt_media_ml=0, media_ml=media_ml / 2, waste_ml=media_ml / 2, log=False)
                self.execute_io_action(alt_media_ml=0, media_ml=media_ml / 2, waste_ml=media_ml / 2, log=False)

            if alt_media_ml > max_:
                self.execute_io_action(alt_media_ml=alt_media_ml / 2, media_ml=0, waste_ml=alt_media_ml / 2, log=False)
                self.execute_io_action(alt_media_ml=alt_media_ml / 2, media_ml=0, waste_ml=alt_media_ml / 2, log=False)
        else:
            if alt_media_ml > 0:
                add_alt_media(ml=alt_media_ml, verbose=self.verbose)
                brief_pause()  # allow time for the addition to mix, and reduce the step response that can cause ringing in the output V.
            if media_ml > 0:
                add_media(ml=media_ml, verbose=self.verbose)
                brief_pause()
            if waste_ml > 0:
                remove_waste(ml=waste_ml, verbose=self.verbose)
                # run remove_waste for an additional second to keep volume constant (determined by the length of the waste tube)
                remove_waste(duration=1, verbose=self.verbose)
                brief_pause()
예제 #6
0
def change_stirring_speed(duty_cycle, unit, verbose=0):
    assert 0 <= duty_cycle <= 100

    publish(f"morbidostat/{unit}/{experiment}/stirring/duty_cycle/set",
            duty_cycle,
            verbose=verbose)
    return
예제 #7
0
        def wrapper(*args, **kwargs):
            from morbidostat.pubsub import publish

            func_name = func.__name__
            publish(f"morbidostat/{unit}/{experiment}/log",
                    f"[{func_name}]: starting.",
                    verbose=1)
            return func(*args, **kwargs)
예제 #8
0
 def set_state(self, new_state):
     if hasattr(self, "state"):
         current_state = self.state
         publish(
             f"morbidostat/{self.unit}/{self.experiment}/log",
             f"[{self.job_name}] Updated state from {current_state} to {new_state}.",
             verbose=self.verbose,
         )
     getattr(self, new_state)()
예제 #9
0
def test_mis_shapen_data(monkeypatch):
    calc = GrowthRateCalculator(unit=unit, experiment=experiment)

    publish(f"morbidostat/{unit}/{experiment}/od_raw_batched",
            '{"135/A": 0.778586260567034, "90/A": 0.1}')
    pause()

    publish(f"morbidostat/{unit}/{experiment}/od_raw_batched",
            '{"135/A": 0.808586260567034}')
    pause()
예제 #10
0
def test_pause_in_io_controlling():

    algo = ControlAlgorithm(target_growth_rate=0.05, target_od=1.0, duration=60, verbose=2, unit=unit, experiment=experiment)
    pause()
    pubsub.publish(f"morbidostat/{unit}/{experiment}/io_controlling/$state/set", "sleeping")
    pause()
    assert algo.state == "sleeping"

    pubsub.publish(f"morbidostat/{unit}/{experiment}/io_controlling/$state/set", "ready")
    pause()
    assert algo.state == "ready"
예제 #11
0
def test_throughput_calculator_restart():
    job_name = "throughput_calculating"

    pubsub.publish(f"morbidostat/{unit}/{experiment}/{job_name}/media_throughput", 1.0, retain=True)
    pubsub.publish(f"morbidostat/{unit}/{experiment}/{job_name}/alt_media_throughput", 1.5, retain=True)

    target_growth_rate = 0.06
    algo = PIDMorbidostat(target_growth_rate=0.05, target_od=1.0, duration=60, verbose=2, unit=unit, experiment=experiment)
    pause()
    assert algo.throughput_calculator.media_throughput == 1.0
    assert algo.throughput_calculator.alt_media_throughput == 1.5
예제 #12
0
def test_states():

    bj = BackgroundJob(job_name="job", unit=unit, experiment=exp)
    pause()
    assert bj.state == "ready"

    publish(f"morbidostat/{unit}/{exp}/job/$state/set", "sleeping")
    pause()
    assert bj.state == "sleeping"

    publish(f"morbidostat/{unit}/{exp}/job/$state/set", "disconnected")
    pause()
예제 #13
0
    def publish_attr(self, attr: str) -> None:
        if attr == "state":
            attr_name = "$state"
        else:
            attr_name = attr

        publish(
            f"morbidostat/{self.unit}/{self.experiment}/{self.job_name}/{attr_name}",
            getattr(self, attr),
            verbose=self.verbose,
            retain=True,
            qos=QOS.EXACTLY_ONCE,
        )
예제 #14
0
def test_changing_morbidostat_parameters_over_mqtt():

    target_growth_rate = 0.05
    algo = PIDMorbidostat(
        target_growth_rate=target_growth_rate, target_od=1.0, duration=60, verbose=2, unit=unit, experiment=experiment
    )
    assert algo.target_growth_rate == target_growth_rate
    pause()
    new_target = 0.07
    pubsub.publish(f"morbidostat/{unit}/{experiment}/io_controlling/target_growth_rate/set", new_target)
    pause()
    assert algo.target_growth_rate == new_target
    assert algo.pid.pid.setpoint == new_target
예제 #15
0
def remove_waste(ml=None, duration=None, duty_cycle=33, verbose=0):
    assert 0 <= duty_cycle <= 100
    assert (ml is not None) or (duration
                                is not None), "Input either ml or duration"
    assert not ((ml is not None) and
                (duration is not None)), "Only input ml or duration"

    hz = 100
    if ml is not None:
        user_submitted_ml = True
        assert ml >= 0
        duration = pump_ml_to_duration(
            ml, duty_cycle,
            **loads(config["pump_calibration"][f"waste{unit}_ml_calibration"]))
    elif duration is not None:
        user_submitted_ml = False
        assert duration >= 0
        ml = pump_duration_to_ml(
            duration, duty_cycle,
            **loads(config["pump_calibration"][f"waste{unit}_ml_calibration"]))

    publish(
        f"morbidostat/{unit}/{experiment}/io_events",
        '{"volume_change": -%0.4f, "event": "remove_waste"}' % ml,
        verbose=verbose,
        qos=QOS.EXACTLY_ONCE,
    )

    try:

        WASTE_PIN = int(config["rpi_pins"]["waste"])
        GPIO.setup(WASTE_PIN, GPIO.OUT)
        GPIO.output(WASTE_PIN, 0)
        pwm = GPIO.PWM(WASTE_PIN, hz)

        pwm.start(duty_cycle)
        time.sleep(duration)
        pwm.stop()

        GPIO.output(WASTE_PIN, 0)

        if user_submitted_ml:
            publish(f"morbidostat/{unit}/{experiment}/log",
                    f"remove waste: {round(ml,2)}mL",
                    verbose=verbose)
        else:
            publish(f"morbidostat/{unit}/{experiment}/log",
                    f"remove waste: {round(duration,2)}s",
                    verbose=verbose)
    except Exception as e:
        publish(f"morbidostat/{unit}/{experiment}/error_log",
                f"[remove_waste]: failed with {str(e)}",
                verbose=verbose)
        raise e

    finally:
        GPIO.cleanup(WASTE_PIN)
    return
예제 #16
0
    def declare_settable_properties_to_broker(self):
        # this follows some of the Homie convention: https://homieiot.github.io/specification/
        publish(
            f"morbidostat/{self.unit}/{self.experiment}/{self.job_name}/$properties",
            ",".join(self.editable_settings),
            verbose=self.verbose,
            qos=QOS.AT_LEAST_ONCE,
        )

        for setting in self.editable_settings:
            publish(
                f"morbidostat/{self.unit}/{self.experiment}/{self.job_name}/{setting}/$settable",
                True,
                verbose=self.verbose,
                qos=QOS.AT_LEAST_ONCE,
            )
예제 #17
0
def test_pause_stirring_mid_cycle():
    original_dc = 50

    st = Stirrer(original_dc, unit, exp, verbose=2)
    assert st.duty_cycle == original_dc
    pause()

    publish(f"morbidostat/{unit}/{exp}/stirring/$state/set", "sleeping")
    pause()

    assert st.duty_cycle == 0

    publish(f"morbidostat/{unit}/{exp}/stirring/$state/set", "ready")
    pause()

    assert st.duty_cycle == 50
예제 #18
0
    def run(self, counter=None):
        if (self.latest_growth_rate is None) or (self.latest_od is None):
            time.sleep(10)  # wait some time for data to arrive, and try again.
            return self.run(counter=counter)

        if self.state != self.READY:
            event = events.NoEvent(f"currently in state {self.state}")

        elif (time.time() - self.most_stale_time) > 5 * 60:
            event = events.NoEvent(
                "readings are too stale (over 5 minutes old) - are `Optical density job` and `Growth rate job` running?"
            )
        else:
            event = self.execute(counter)

        publish(f"morbidostat/{self.unit}/{self.experiment}/log", f"[{JOB_NAME}]: triggered {event}.", verbose=self.verbose)
        return event
예제 #19
0
def add_alt_media(ml=None, duration=None, duty_cycle=33, verbose=0):
    assert 0 <= duty_cycle <= 100
    assert (ml is not None) or (duration is not None)
    assert not ((ml is not None) and
                (duration is not None)), "Only select ml or duration"

    hz = 100
    if ml is not None:
        assert ml >= 0
        duration = pump_ml_to_duration(
            ml, duty_cycle,
            **loads(
                config["pump_calibration"][f"alt_media{unit}_ml_calibration"]))
    elif duration is not None:
        ml = pump_duration_to_ml(
            duration, duty_cycle,
            **loads(
                config["pump_calibration"][f"alt_media{unit}_ml_calibration"]))
    assert duration >= 0

    publish(
        f"morbidostat/{unit}/{experiment}/io_events",
        '{"volume_change": %0.4f, "event": "add_alt_media"}' % ml,
        verbose=verbose,
        qos=QOS.EXACTLY_ONCE,
    )

    try:

        ALT_MEDIA_PIN = int(config["rpi_pins"]["alt_media"])
        GPIO.setup(ALT_MEDIA_PIN, GPIO.OUT)
        GPIO.output(ALT_MEDIA_PIN, 0)
        pwm = GPIO.PWM(ALT_MEDIA_PIN, hz)

        pwm.start(duty_cycle)
        time.sleep(duration)
        pwm.stop()

        GPIO.output(ALT_MEDIA_PIN, 0)

        if ml is not None:
            publish(f"morbidostat/{unit}/{experiment}/log",
                    f"add alt media: {round(ml,2)}mL",
                    verbose=verbose)
        else:
            publish(f"morbidostat/{unit}/{experiment}/log",
                    f"add alt media: {round(duration,2)}s",
                    verbose=verbose)
    except Exception as e:
        publish(f"morbidostat/{unit}/{experiment}/error_log",
                f"[add_alt_media]: failed with {str(e)}",
                verbose=verbose)
        raise e
    finally:
        GPIO.cleanup(ALT_MEDIA_PIN)
    return
예제 #20
0
def test_silent_algorithm():
    io = io_controlling(mode="silent", volume=None, duration=60, verbose=2)
    pause()
    pubsub.publish(f"morbidostat/{unit}/{experiment}/growth_rate", "0.01")
    pubsub.publish(f"morbidostat/{unit}/{experiment}/od_filtered/135/A", "1.0")
    pause()
    assert isinstance(next(io), events.NoEvent)

    pubsub.publish(f"morbidostat/{unit}/{experiment}/growth_rate", "0.02")
    pubsub.publish(f"morbidostat/{unit}/{experiment}/od_filtered/135/A", "1.1")
    pause()
    assert isinstance(next(io), events.NoEvent)
    def publish_pid_stats(self):
        from morbidostat.pubsub import publish

        to_send = {
            "setpoint": self.pid.setpoint,
            "output_limits_lb": self.pid.output_limits[0],
            "output_limits_ub": self.pid.output_limits[1],
            "Kd": self.pid.Kd,
            "Ki": self.pid.Ki,
            "Kp": self.pid.Kp,
            "integral": self.pid._integral,
            "proportional": self.pid._proportional,
            "derivative": self.pid._derivative,
            "latest_input": self.pid._last_input,
            "latest_output": self.pid._last_output,
        }
        publish(f"morbidostat/{self.unit}/{self.experiment}/pid_log",
                json.dumps(to_send),
                verbose=self.verbose)
예제 #22
0
def test_change_stirring_mid_cycle():
    original_dc = 50

    st = Stirrer(original_dc, unit, exp, verbose=2)
    assert st.duty_cycle == original_dc
    pause()

    new_dc = 75
    publish(f"morbidostat/{unit}/{exp}/stirring/duty_cycle/set", new_dc)

    pause()

    assert st.duty_cycle == new_dc
    assert st.state == "ready"

    publish(f"morbidostat/{unit}/{exp}/stirring/duty_cycle/set", 0)
    pause()
    assert st.duty_cycle == 0
    assert st.state == "sleeping"
    pause()
예제 #23
0
    def update_media_throughput(self, media_delta, alt_media_delta):

        self._alt_media_throughput += alt_media_delta
        self._media_throughput += media_delta

        publish(
            f"morbidostat/{self.unit}/{self.experiment}/{self.job_name}/media_throughput",
            self.media_throughput,
            verbose=self.verbose,
            retain=True,
            qos=QOS.EXACTLY_ONCE,
        )

        publish(
            f"morbidostat/{self.unit}/{self.experiment}/{self.job_name}/alt_media_throughput",
            self.alt_media_throughput,
            verbose=self.verbose,
            retain=True,
            qos=QOS.EXACTLY_ONCE,
        )
        return
def download_experiment_data(experiment, output, tables):
    import pandas as pd
    import sqlite3

    if not whoami.am_I_leader():
        print(
            f"This command should be run on the {config.leader_hostname} node, not worker."
        )
        return

    publish(f"morbidostat/{whoami.unit}/{whoami.experiment}/log",
            f"Starting export of experiment data to {output}.",
            verbose=1)

    if experiment == "current":
        experiment = get_latest_experiment_name()

    time = datetime.strformat("YYYYMMDDHHmmSS")
    zf = zipfile.ZipFile("{output}/{experiment}-{time}.zip", "w",
                         zipfile.ZIP_DEFLATED)
    con = sqlite3.connect(config["data"]["observation_database"])

    for table in tables:
        df = pd.read_sql_query(
            f"""
            SELECT * from {table} WHERE experiment="{experiment}"
        """,
            con,
        )

        filename = "{experiment}-{table}-{time}.dump.csv.gz"
        df.to_csv(filename, compression="gzip", index=False)
        zf.write(filename)

    zf.close()

    publish(f"morbidostat/{whoami.unit}/{whoami.experiment}/log",
            f"Completed export of experiment data to {output}.",
            verbose=1)
    return
    def update_state_from_observation(self, message):
        if self.state != self.READY:
            return

        if self.ekf is None:
            # pass to wait for state to initialize
            return

        try:
            observations = self.json_to_sorted_dict(message.payload)
            scaled_observations = self.scale_raw_observations(observations)
            self.ekf.update(list(scaled_observations.values()))

            publish(
                f"morbidostat/{self.unit}/{self.experiment}/growth_rate",
                self.multiplicative_rate_to_exp_rate(self.state_[-1]),
                verbose=self.verbose,
                retain=True,
            )

            for i, angle_label in enumerate(self.angles):
                publish(
                    f"morbidostat/{self.unit}/{self.experiment}/od_filtered/{angle_label}",
                    self.state_[i],
                    verbose=self.verbose)

            return

        except Exception as e:
            publish(
                f"morbidostat/{self.unit}/{self.experiment}/error_log",
                f"[{JOB_NAME}]: failed {e}. Skipping.",
                verbose=self.verbose,
            )
예제 #26
0
def io_controlling(mode=None, duration=None, verbose=0, sensor="135/A", skip_first_run=False, **kwargs) -> Iterator[events.Event]:

    algorithms = {
        "silent": Silent,
        "morbidostat": Morbidostat,
        "turbidostat": Turbidostat,
        "pid_turbidostat": PIDTurbidostat,
        "pid_morbidostat": PIDMorbidostat,
    }

    assert mode in algorithms.keys()

    publish(
        f"morbidostat/{unit}/{experiment}/log",
        f"[{JOB_NAME}]: starting {mode} with {duration}min intervals, metadata: {kwargs}",
        verbose=verbose,
    )

    if skip_first_run:
        publish(f"morbidostat/{unit}/{experiment}/log", f"[{JOB_NAME}]: skipping first run", verbose=verbose)
        time.sleep(duration * 60)

    kwargs["verbose"] = verbose
    kwargs["duration"] = duration
    kwargs["unit"] = unit
    kwargs["experiment"] = experiment
    kwargs["sensor"] = sensor

    algo = algorithms[mode](**kwargs)

    def _gen():
        try:
            yield from every(duration * 60, algo.run)
        except Exception as e:
            publish(f"morbidostat/{unit}/{experiment}/error_log", f"[{JOB_NAME}]: failed {str(e)}", verbose=verbose)
            raise e

    return _gen()
예제 #27
0
def test_execute_io_action():
    pubsub.publish(f"morbidostat/{unit}/{experiment}/throughput_calculating/media_throughput", None, retain=True)
    pubsub.publish(f"morbidostat/{unit}/{experiment}/throughput_calculating/alt_media_throughput", None, retain=True)
    ca = ControlAlgorithm(verbose=2, unit=unit, experiment=experiment)
    ca.execute_io_action(media_ml=0.65, alt_media_ml=0.35, waste_ml=0.65 + 0.35)
    pause()
    assert ca.throughput_calculator.media_throughput == 0.65
    assert ca.throughput_calculator.alt_media_throughput == 0.35

    ca.execute_io_action(media_ml=0.15, alt_media_ml=0.15, waste_ml=0.3)
    pause()
    assert ca.throughput_calculator.media_throughput == 0.80
    assert ca.throughput_calculator.alt_media_throughput == 0.50

    ca.execute_io_action(media_ml=1.0, alt_media_ml=0, waste_ml=1)
    pause()
    assert ca.throughput_calculator.media_throughput == 1.80
    assert ca.throughput_calculator.alt_media_throughput == 0.50

    ca.execute_io_action(media_ml=0.0, alt_media_ml=1.0, waste_ml=1)
    pause()
    assert ca.throughput_calculator.media_throughput == 1.80
    assert ca.throughput_calculator.alt_media_throughput == 1.50
예제 #28
0
    def __init__(self, target_growth_rate=None, target_od=None, duration=None, volume=None, verbose=0, **kwargs):
        super(PIDMorbidostat, self).__init__(verbose=verbose, **kwargs)
        assert target_od is not None, "`target_od` must be set"
        assert target_growth_rate is not None, "`target_growth_rate` must be set"
        assert duration is not None, "`duration` must be set"

        self.display_name = "Morbidostat"
        self.target_growth_rate = target_growth_rate
        self.target_od = target_od
        self.duration = duration

        self.pid = PID(
            -0.5, -0.0001, -0.25, setpoint=self.target_growth_rate, output_limits=(0, 1), sample_time=None, verbose=self.verbose
        )

        if volume is not None:
            publish(
                f"morbidostat/{self.unit}/{self.experiment}/log",
                f"[{JOB_NAME}]: Ignoring volume parameter; volume set by target growth rate and duration.",
                verbose=self.verbose,
            )

        self.volume = self.target_growth_rate * VIAL_VOLUME * (self.duration / 60)
        self.verbose = verbose
예제 #29
0
def test_pid_turbidostat_algorithm():

    target_od = 2.4
    algo = io_controlling(mode="pid_turbidostat", target_od=target_od, volume=2.0, duration=30, verbose=2)

    pubsub.publish(f"morbidostat/{unit}/{experiment}/growth_rate", 0.01)
    pubsub.publish(f"morbidostat/{unit}/{experiment}/od_filtered/135/A", 3.2)
    pause()
    e = next(algo)
    assert isinstance(e, events.DilutionEvent)
    assert e.volume_to_cycle > 1.0

    pubsub.publish(f"morbidostat/{unit}/{experiment}/growth_rate", 0.01)
    pubsub.publish(f"morbidostat/{unit}/{experiment}/od_filtered/135/A", 3.1)
    pause()
    e = next(algo)
    assert isinstance(e, events.DilutionEvent)
    assert e.volume_to_cycle > 1.0
예제 #30
0
def test_same_angles(monkeypatch):
    calc = GrowthRateCalculator(unit=unit, experiment=experiment)
    publish(
        f"morbidostat/{unit}/{experiment}/od_raw_batched",
        '{"135/A": 0.778586260567034, "135/B": 0.20944389172032837, "90/A": 0.1}',
    )
    publish(
        f"morbidostat/{unit}/{experiment}/od_raw_batched",
        '{"135/A": 0.808586260567034, "135/B": 0.21944389172032837, "90/A": 0.2}',
    )
    publish(
        f"morbidostat/{unit}/{experiment}/od_raw_batched",
        '{"135/A": 0.808586260567034, "135/B": 0.21944389172032837, "90/A": 0.2}',
    )