def test_changing_algo_over_mqtt_with_wrong_type_is_okay() -> None:
    with local_persistant_storage("media_throughput") as c:
        c[experiment] = "0.0"

    algo = DosingController(
        "pid_turbidostat",
        volume=1.0,
        target_od=0.4,
        duration=2 / 60,
        unit=unit,
        experiment=experiment,
    )
    assert algo.automation["automation_name"] == "pid_turbidostat"
    assert algo.automation_name == "pid_turbidostat"
    pause()
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_control/automation/set",
        '{"automation_name": "pid_turbidostat", "duration": "60", "target_od": "1.0", "volume": "1.0"}',
    )
    time.sleep(
        7
    )  # need to wait for all jobs to disconnect correctly and threads to join.
    assert isinstance(algo.automation_job, PIDTurbidostat)
    assert algo.automation_job.target_od == 1.0
    algo.set_state(algo.DISCONNECTED)
def test_custom_class_will_register_and_run() -> None:
    class NaiveTurbidostat(DosingAutomation):

        automation_name = "naive_turbidostat"
        published_settings = {
            "target_od": {
                "datatype": "float",
                "settable": True,
                "unit": "AU"
            },
            "duration": {
                "datatype": "float",
                "settable": True,
                "unit": "min"
            },
        }

        def __init__(self, target_od: float, **kwargs: Any) -> None:
            super(NaiveTurbidostat, self).__init__(**kwargs)
            self.target_od = target_od

        def execute(self) -> None:
            if self.latest_od > self.target_od:
                self.execute_io_action(media_ml=1.0, waste_ml=1.0)

    algo = DosingController(
        "naive_turbidostat",
        target_od=2.0,
        duration=10,
        unit=get_unit_name(),
        experiment=get_latest_experiment_name(),
    )
    algo.set_state(algo.DISCONNECTED)
def test_changing_algo_over_mqtt_will_not_produce_two_dosing_jobs() -> None:
    with local_persistant_storage("media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_fraction") as c:
        c[experiment] = "0.0"

    algo = DosingController(
        "pid_turbidostat",
        volume=1.0,
        target_od=0.4,
        duration=60,
        unit=unit,
        experiment=experiment,
    )
    assert algo.automation["automation_name"] == "pid_turbidostat"
    pause()
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_control/automation/set",
        '{"automation_name": "turbidostat", "duration": 60, "target_od": 1.0, "volume": 1.0, "skip_first_run": 1}',
    )
    time.sleep(
        10
    )  # need to wait for all jobs to disconnect correctly and threads to join.
    assert isinstance(algo.automation_job, Turbidostat)

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/growth_rate",
        '{"growth_rate": 1.0}',
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/od_filtered",
        '{"od_filtered": 1.0}',
    )
    pause()

    # note that we manually run, as we have skipped the first run in the json
    algo.automation_job.run()
    time.sleep(5)
    assert algo.automation_job.media_throughput == 1.0

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_automation/target_od/set", 1.5)
    pause()
    pause()
    assert algo.automation_job.target_od == 1.5
    algo.set_state(algo.DISCONNECTED)
def test_execute_io_action() -> None:
    with local_persistant_storage("media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_throughput") as c:
        c[experiment] = "0.0"

    with DosingController("silent", unit=unit, experiment=experiment) as ca:
        ca.automation_job.execute_io_action(media_ml=0.65,
                                            alt_media_ml=0.35,
                                            waste_ml=0.65 + 0.35)
        pause()
        assert ca.automation_job.media_throughput == 0.65
        assert ca.automation_job.alt_media_throughput == 0.35

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

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

        ca.automation_job.execute_io_action(media_ml=0.0,
                                            alt_media_ml=1.0,
                                            waste_ml=1)
        pause()
        assert ca.automation_job.media_throughput == 1.80
        assert ca.automation_job.alt_media_throughput == 1.50
def test_changing_algo_over_mqtt_with_wrong_type_is_okay():
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/throughput_calculating/media_throughput",
        None,
        retain=True,
    )

    algo = DosingController(
        "pid_turbidostat",
        volume=1.0,
        target_od=0.4,
        duration=2 / 60,
        unit=unit,
        experiment=experiment,
    )
    assert algo.dosing_automation == "pid_turbidostat"
    pause()
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_control/dosing_automation/set",
        '{"dosing_automation": "pid_turbidostat", "duration": "60", "target_od": "1.0", "volume": "1.0"}',
    )
    time.sleep(
        7
    )  # need to wait for all jobs to disconnect correctly and threads to join.
    assert isinstance(algo.dosing_automation_job, PIDTurbidostat)
    assert algo.dosing_automation_job.target_od == 1.0
def test_throughput_calculator_manual_set() -> None:

    with local_persistant_storage("media_throughput") as c:
        c[experiment] = str(1.0)

    with local_persistant_storage("alt_media_throughput") as c:
        c[experiment] = str(1.5)

    with DosingController(
            "turbidostat",
            target_od=1.0,
            duration=5 / 60,
            volume=1.0,
            unit=unit,
            experiment=experiment,
    ) as algo:

        pause()
        assert algo.automation_job.media_throughput == 1.0
        assert algo.automation_job.alt_media_throughput == 1.5

        pubsub.publish(
            f"pioreactor/{unit}/{experiment}/dosing_automation/alt_media_throughput/set",
            0,
        )
        pubsub.publish(
            f"pioreactor/{unit}/{experiment}/dosing_automation/media_throughput/set",
            0)
        pause()
        pause()
        assert algo.automation_job.media_throughput == 0
        assert algo.automation_job.alt_media_throughput == 0
def test_changing_algo_over_mqtt_when_it_fails_will_rollback():

    algo = DosingController(
        "turbidostat",
        target_od=1.0,
        duration=5 / 60,
        volume=1.0,
        unit=unit,
        experiment=experiment,
    )
    assert algo.dosing_automation == "turbidostat"
    assert isinstance(algo.dosing_automation_job, Turbidostat)
    pause()
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_control/dosing_automation/set",
        '{"dosing_automation": "pid_morbidostat", "duration": 60}',
    )
    time.sleep(8)
    assert algo.dosing_automation == "turbidostat"
    assert isinstance(algo.dosing_automation_job, Turbidostat)
    assert algo.dosing_automation_job.target_od == 1.0
    algo.set_state("disconnected")
def test_changing_algo_over_mqtt_solo():

    algo = DosingController(
        "turbidostat",
        target_od=1.0,
        duration=5 / 60,
        volume=1.0,
        unit=unit,
        experiment=experiment,
    )
    assert algo.dosing_automation == "turbidostat"
    assert isinstance(algo.dosing_automation_job, Turbidostat)

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_control/dosing_automation/set",
        '{"dosing_automation": "pid_morbidostat", "duration": 60, "target_od": 1.0, "target_growth_rate": 0.07}',
    )
    time.sleep(8)
    assert algo.dosing_automation == "pid_morbidostat"
    assert isinstance(algo.dosing_automation_job, PIDMorbidostat)
    assert algo.dosing_automation_job.target_growth_rate == 0.07
    algo.set_state("disconnected")
def test_pause_in_dosing_control_also_pauses_automation() -> None:

    algo = DosingController(
        "turbidostat",
        target_od=1.0,
        duration=5 / 60,
        volume=1.0,
        unit=unit,
        experiment=experiment,
    )
    pause()
    pubsub.publish(f"pioreactor/{unit}/{experiment}/dosing_control/$state/set",
                   "sleeping")
    pause()
    assert algo.state == "sleeping"
    assert algo.automation_job.state == "sleeping"

    pubsub.publish(f"pioreactor/{unit}/{experiment}/dosing_control/$state/set",
                   "ready")
    pause()
    assert algo.state == "ready"
    assert algo.automation_job.state == "ready"
    algo.set_state(algo.DISCONNECTED)
def test_changing_algo_over_mqtt_will_not_produce_two_dosing_jobs():
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/throughput_calculating/media_throughput",
        None,
        retain=True,
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/throughput_calculating/alt_media_throughput",
        None,
        retain=True,
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/alt_media_calculating/alt_media_fraction",
        None,
        retain=True,
    )

    algo = DosingController(
        "pid_turbidostat",
        volume=1.0,
        target_od=0.4,
        duration=60,
        unit=unit,
        experiment=experiment,
    )
    assert algo.dosing_automation == "pid_turbidostat"
    pause()
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_control/dosing_automation/set",
        '{"dosing_automation": "turbidostat", "duration": 60, "target_od": 1.0, "volume": 1.0, "skip_first_run": 1}',
    )
    time.sleep(
        10
    )  # need to wait for all jobs to disconnect correctly and threads to join.
    assert isinstance(algo.dosing_automation_job, Turbidostat)

    pubsub.publish(f"pioreactor/{unit}/{experiment}/growth_rate", 0.15)
    pubsub.publish(f"pioreactor/{unit}/{experiment}/od_filtered/135/0", 1.5)
    pause()

    # note that we manually run, as we have skipped the first run in the json
    algo.dosing_automation_job.run()
    time.sleep(5)
    assert algo.dosing_automation_job.throughput_calculator.media_throughput == 1.0

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/dosing_automation/target_od/set", 1.5)
    pause()
    pause()
    assert algo.dosing_automation_job.target_od == 1.5
def test_disconnect_cleanly():

    algo = DosingController(
        "turbidostat",
        target_od=1.0,
        duration=5 / 60,
        unit=unit,
        volume=1.0,
        experiment=experiment,
    )
    assert algo.dosing_automation == "turbidostat"
    assert isinstance(algo.dosing_automation_job, Turbidostat)
    pubsub.publish(f"pioreactor/{unit}/{experiment}/dosing_control/$state/set",
                   "disconnected")
    time.sleep(10)
def test_execute_io_action2() -> None:
    with local_persistant_storage("media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_fraction") as c:
        c[experiment] = "0.0"

    with DosingController("silent", unit=unit, experiment=experiment) as ca:
        ca.automation_job.execute_io_action(media_ml=1.25,
                                            alt_media_ml=0.01,
                                            waste_ml=1.26)
        pause()
        assert ca.automation_job.media_throughput == 1.25
        assert ca.automation_job.alt_media_throughput == 0.01
        assert abs(ca.automation_job.alt_media_fraction - 0.0007142) < 0.000001
def test_throughput_calculator_restart() -> None:

    with local_persistant_storage("media_throughput") as c:
        c[experiment] = str(1.0)

    with local_persistant_storage("alt_media_throughput") as c:
        c[experiment] = str(1.5)

    with DosingController(
            "turbidostat",
            target_od=1.0,
            duration=5 / 60,
            volume=1.0,
            unit=unit,
            experiment=experiment,
    ) as algo:
        pause()
        assert algo.automation_job.media_throughput == 1.0
        assert algo.automation_job.alt_media_throughput == 1.5
def test_changing_algo_over_mqtt_when_it_fails_will_rollback() -> None:

    with DosingController(
            "turbidostat",
            target_od=1.0,
            duration=5 / 60,
            volume=1.0,
            unit=unit,
            experiment=experiment,
    ) as algo:
        assert algo.automation["automation_name"] == "turbidostat"
        assert isinstance(algo.automation_job, Turbidostat)
        pause()
        pubsub.publish(
            f"pioreactor/{unit}/{experiment}/dosing_control/automation/set",
            '{"automation_name": "pid_morbidostat", "duration": 60}',
        )
        time.sleep(10)
        assert algo.automation["automation_name"] == "turbidostat"
        assert isinstance(algo.automation_job, Turbidostat)
        assert algo.automation_job.target_od == 1.0
        pause()
        pause()
        pause()
def test_throughput_calculator() -> None:
    with local_persistant_storage("media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_throughput") as c:
        c[experiment] = "0.0"

    with local_persistant_storage("alt_media_fraction") as c:
        c[experiment] = "0.0"

    algo = DosingController(
        "pid_morbidostat",
        target_growth_rate=0.05,
        target_od=1.0,
        duration=60,
        unit=unit,
        experiment=experiment,
    )
    assert algo.automation_job.media_throughput == 0
    pause()
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/growth_rate",
        '{"growth_rate": 0.08}',
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/od_filtered",
        '{"od_filtered": 1.0}',
    )
    pause()
    algo.automation_job.run()

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/growth_rate",
        '{"growth_rate": 0.08}',
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/od_filtered",
        '{"od_filtered": 0.95}',
    )
    pause()
    algo.automation_job.run()
    assert algo.automation_job.media_throughput > 0
    assert algo.automation_job.alt_media_throughput > 0

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/growth_rate",
        '{"growth_rate": 0.07}',
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/od_filtered",
        '{"od_filtered": 0.95}',
    )
    pause()
    algo.automation_job.run()
    assert algo.automation_job.media_throughput > 0
    assert algo.automation_job.alt_media_throughput > 0

    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/growth_rate",
        '{"growth_rate": 0.065}',
    )
    pubsub.publish(
        f"pioreactor/{unit}/{experiment}/growth_rate_calculating/od_filtered",
        '{"od_filtered": 0.95}',
    )
    pause()
    algo.automation_job.run()
    assert algo.automation_job.media_throughput > 0
    assert algo.automation_job.alt_media_throughput > 0
    algo.set_state(algo.DISCONNECTED)