目次 — 知りたいところから読む

CEAの養液タンク — 水量・濃度と補給操作の収支

水位と濃度を同時に戻すには

90 Lの養液に、追跡している成分が80 mg/L含まれている。水を10 L足せば水位は100 Lへ戻るが、濃度は72 mg/Lへ下がる。「水が少ない」と「成分が足りない」を一つの補給指令にまとめると、この違いを見落とす。

根圏の基礎で整理した水と溶質の帳簿を、今回は操作の前後へつなぐ。すべての数値は説明用の合成条件であり、100 mg/Lは作物の推奨値ではない。扱うのは既知濃度の一成分で、肥料全体の処方、ECからの濃度推定、酸・塩基の投入量は計算しない。

計算の境界を決める

ここでは栽培槽からの送り戻りを止めた、完全混合の一槽を考える。操作中の植物吸収、蒸発、沈殿、反応はゼロ、液の体積は加算可能と仮定する。実際に運転中のタンクへ使うなら、送りと戻りの体積・成分量も追加する必要がある。循環系全体を境界にするなら、内部の循環を外部補給として足さない。

体積 V [L]、濃度 C [mg/L]、液相の成分質量 M [mg]について M=VC である。濃度 C_a の液を V_a 加えた後は、

V_1=V_0+V_a,\qquad M_1=V_0C_0+V_aC_a,\qquad C_1=\frac{M_1}{V_1}

対象成分を含まない水なら C_a=0。原水に成分が含まれる場合はゼロに置かない。OSUの養液管理資料は、原水分析と、塩濃度・pH・アルカリ度・養分比率を確認する必要を説明している。

三つの補給方法を比べる

初期状態を90 L、80 mg/L、したがって7,200 mgとする。対象成分の濃縮液は10,000 mg/Lと仮定する。

操作 最終体積 最終成分質量 最終濃度
対象成分ゼロの水を10 L追加 100 L 7,200 mg 72.00 mg/L
濃縮液だけ約0.181818 L追加 約90.181818 L 約9,018.18 mg 100.00 mg/L
濃縮液0.28 Lと水9.72 L追加 100 L 10,000 mg 100.00 mg/L

二行目では濃縮液自体の体積も増える。追加量 x を求めるには、目標濃度 C_t と濃縮液濃度 C_s を使って、

\frac{V_0C_0+xC_s}{V_0+x}=C_t \quad\Rightarrow\quad x=\frac{V_0(C_t-C_0)}{C_s-C_t}

濃度を上げる場合は C_s>C_t>C_0 が必要である。濃縮液が目標と同じ濃度なら有限量では到達しない。負の解が出たときは、負の投入を実行するのではなく、その操作では目的を達成できないと読む。

三行目は最終体積 V_f も固定した例である。対象成分ゼロの水を使うなら、

x=\frac{V_fC_t-V_0C_0}{C_s},\qquad V_{water}=V_f-V_0-x

どちらの追加量も非負で、容器の許容体積内に収まることを確かめる。ここでは x=0.28 L、V_{water}=9.72 Lとなる。濃度だけを合わせる計算と、体積も指定する計算は解が違う。

排液と希釈は同じ操作ではない

今度は100 L、120 mg/L、12,000 mgから始める。均一に混ざった液10 Lを排出すると、成分も1,200 mg出る。残りは90 L、10,800 mgで、濃度は120 mg/Lのままである。その後、対象成分ゼロの水10 Lを入れると100 L、108 mg/Lになる。

一方、排液せずに水20 Lを加えると120 L、100 mg/Lになる。濃度を下げても成分質量は12,000 mgのままであり、必要な空き容量も異なる。

条件 使う式 注意する境界
完全混合の液を先に D L排出し、同量の水で補給 C_1=C_0(1-D/V_0) 排液後に補給。対象成分ゼロの水
排液せず水 W Lで希釈 C_1=V_0C_0/(V_0+W) タンク体積が増える
同時に流入・流出、体積一定 C(t)=C_{in}+(C_0-C_{in})e^{-Qt/V} 完全混合、一定流量、吸収・反応なし

同時に10 L流して置き換える場合、Qt/V=0.1、C_{in}=0 なら約108.58 mg/Lとなる。先に10 L排出する場合の108 mg/Lとは一致しない。投入と排出の順番をログへ残す理由がここにある。

Pythonで前後の帳簿を再現する

次を tank_accounting.py に保存し、python3 tank_accounting.py で実行する。標準ライブラリだけを使う。

from math import isfinite


def mix(v, c, added_v, added_c):
    values = (v, c, added_v, added_c)
    if not all(isfinite(x) for x in values):
        raise ValueError("finite values required")
    if v <= 0 or min(c, added_v, added_c) < 0:
        raise ValueError("invalid volume or concentration")
    return (v * c + added_v * added_c) / (v + added_v)


v, c, target, stock = 90.0, 80.0, 100.0, 10000.0
x = v * (target - c) / (stock - target)
print(f"water top-up: {mix(v, c, 10, 0):.2f} mg/L")
print(f"stock addition: {x:.6f} L, {mix(v, c, x, stock):.2f} mg/L")
y = (100 * target - v * c) / stock
print(f"fixed final volume: stock {y:.2f} L, water {10-y:.2f} L")
print(f"drain then refill: {mix(90, 120, 10, 0):.2f} mg/L")
print(f"dilute only: {mix(100, 120, 20, 0):.2f} mg/L")

出力は順に72.00 mg/L、0.181818 Lと100.00 mg/L、0.28 Lと9.72 L、108.00 mg/L、100.00 mg/Lとなる。濃縮液濃度を変える練習では、式の分母がゼロでないことと、上で示した到達条件を先に確認する。

収支から制御へ進む前に

OSUの水質検査解説が説明するように、ECやTDSは個々の塩の濃度を示さない。したがって上の C にEC値を代入して、特定成分の補給量を求めることはできない。実際の複合肥料では同じ投入が複数の成分を同時に変えるため、一成分の目標達成は組成全体の復元を意味しない。

また、完全混合の計算値へ瞬時にセンサー値が到達するとは限らない。OSUの養液管理資料でも、攪拌して読みが安定してから確認する手順が示されている。本記事では、その知見を踏まえた制御設計の下書きとして次を提案する。

  1. 操作前の時刻、水量、測定場所、濃度の根拠、EC・液温・pHを記録する。
  2. 補給液の濃度と実際の投入量、排液量、送り戻りの状態を記録する。ポンプの指令時間だけで実流量を確定しない。
  3. 投入後は設備で確認した混合時間と測定の安定を待ち、再測定する。投入直後の局所値に反応して追加指令を重ねない。
  4. 計算との差を残し、水位異常・流量不一致・古い測定値では次の投入を止めて調べる。許容差や上限量は実設備で定める。

これは自動投入装置の完成仕様ではない。今回確かめられたのは、指定した境界と仮定の中で水量・成分量・濃度が整合することまでである。次は実測したポンプ流量と混合応答を使い、操作量と観測値を結ぶ段階になる。

CEAの養液ポンプ — 吐出量校正と混合待ち時間

「20 mL入れた」と言うために必要な二つの確認

前の記事では、必要な補給量を水と成分の収支から計算した。しかし20 mLという計算結果は、そのままポンプの運転時間にはならない。さらに、吐出が終わっても、タンクの測定値が代表的な値になったとは限らない。

今回は指令時間から実際の吐出量を求める校正と、投入後に再測定できる時点を決める混合応答の確認を分けて扱う。掲載データはすべて独自の合成例であり、特定の装置・作物の実測性能ではない。実機を動かすコードや肥料処方は含まない。

校正の前に運転条件を固定する

University of Georgiaの肥料注入器ガイドは、注入器の定期的な校正・保守と、方式ごとの流量範囲や圧力条件を説明している。水流に比例する注入器の注入比と、ここで扱う時間指令式ポンプのmL/sは異なる量である。

本記事では、採液によって指令と実量を対応付ける実験記録を提案する。機種の取扱説明書に従い、運転状態をそろえる。

記録項目 比較で必要な理由
ポンプ型式、速度設定、チューブ・弁、使用履歴 校正した構成を特定する
液種、温度、液密度を使った場合の値と根拠 水で得た結果を別の濃縮液へ自動的に移さない
吸込側液面、吐出先の高さ・圧力、配管 実運転と採液試験の条件差を残す
呼び水・気泡、停止からの待ち時間 始動条件の違いを切り分ける
指令開始・停止時刻、採液終了時刻、採液量 停止後の滴下を数える範囲をそろえる

採液のために吐出配管を外すと、背圧まで変わることがある。採液しやすい条件の値だけで実系への投入量を保証しない。水で予備確認する場合も、対象液での確認とは分ける。

質量で量る場合は、容器の風袋を除いた増分 \Delta m [g] と液密度 \rho [g/mL]から V=\Delta m/\rho [mL]とする。すべての液を1 g/mLとはしない。計量器の分解能・校正状態と時刻の精度も記録する。

長時間の平均流量だけでは短い投入を説明できない

同じ状態から始動し、各指令時間を3回ずつ試したと仮定する。

指令時間 [s] 合成採液量3回 [mL] 平均 [mL]
10 15, 16, 17 16
20 35, 36, 37 36
30 55, 56, 57 56
60 115, 116, 117 116

この平均値には V(t)=at+b=2t-4 [mL]が一致する。a はmL/s、b はmLである。20 mLを得る計算上の指令は (20-b)/a=12 sになる。傾き2 mL/sだけを使って10 sとすると、この合成例では16 mLしか出ない。

一方、60 sの平均流量116/60を一定流量として使うと、20 mLへの指令は約10.34 sとなり、同じ式での吐出量は約16.69 mLとなる。どの「流量」を用いたかが結果を変える。

切片−4 mLは、この範囲の回帰係数である。必ず2 sの純粋な遅れがあると断定しない。10〜60 sの外側、特にゼロ付近への外挿は禁止する。実測では残差を見て、短時間域が曲がるなら範囲を分けるなど別のモデルを検討する。

各行の合成値の標本標準偏差は1 mLである。平均への回帰誤差がゼロでも、個々の吐出のばらつきがゼロになったわけではない。また3回の繰り返しでは、日間差や計量器の偏りまで評価できない。実測検証では、回帰に使わない指令時間と別の試行で予測誤差を確認する。

吐出完了と混合確認を別の時刻にする

OSUの養液管理資料は、攪拌して測定値が安定してから確認する手順を示している。待ち時間を決めるには、投入量の校正とは別に、投入後の時系列を調べる。

実設備での確認では、投入点付近だけでなく離れた場所でも同期して記録する。液量、循環流量、投入位置、測定位置、液温、センサーのフィルタ設定を固定・記録する。測定値の変化には混合、移送、センサー自身の応答が重なるため、一つの応答時間を純粋な混合時間と決めつけない。

ここでは初期値 y_0 から既知の最終値 y_\infty への変化を正規化し、説明用に次を生成する。

z(t)=\frac{y(t)-y_0}{y_\infty-y_0}=1-e^{-t/\tau}

時刻ゼロは投入終了である。これは局所測定値の合成応答で、投入質量を保存するタンクモデルではない。二地点A・Bの時定数を40 s・80 sと仮定すると、最終変化量の5%以内に入る連続時間は約120 s・240 sとなる。

図1 · ボタンで表示を切り替え
合成データによる指令時間と採液量、および二地点の正規化応答。下図の破線は例示用の95%到達線。

上:3回の採液値と平均への直線。下:合成応答A・B。横軸は秒、上図の縦軸はmL、下図の縦軸は無次元の応答。実測データではない。

今回はさらに、10 sごとのサンプルで両地点が5%以内に30 s続けて入ったことを確認するという教材用ルールを置く。Bが初めて入るのは240 s、240・250・260・270 sの4点がそろう確認時刻は270 sになる。Aだけを見ると150 sで条件を満たし、Bの状態を見落とす。

5%と30 sは一般的な許容基準ではない。実測ではセンサーのノイズ・精度、許容する濃度偏差、サンプル間隔に応じて決める。最終値は十分長い記録などで独立に推定し、地点間の最終値自体の差も見る。最終値を地点ごとに正規化するだけでは、空間的な濃度差を隠すことがある。初期値と最終値が近く、ノイズに対して変化が小さい場合は正規化による判定を採用しない。

Pythonで計算を再現する

pump_mixing.py として保存し、python3 pump_mixing.py で実行できる。標準ライブラリのみを使う。

from math import exp
from statistics import mean

# Synthetic data only; time [s], collected volume [mL].
runs = {10: [15, 16, 17], 20: [35, 36, 37],
        30: [55, 56, 57], 60: [115, 116, 117]}
ts = list(runs)
vs = [mean(runs[t]) for t in ts]
tm, vm = mean(ts), mean(vs)
a = sum((t-tm)*(v-vm) for t, v in zip(ts, vs)) / sum((t-tm)**2 for t in ts)
b = vm - a*tm
command = (20-b)/a
if not min(ts) <= command <= max(ts):
    raise ValueError("command outside calibration range")
print(f"fit: V = {a:.2f} t {b:+.2f}; 20 mL command = {command:.2f} s")
print(f"held-out 40 s: predicted {a*40+b:.2f} mL; reference 76.00 mL")

# Two synthetic normalized responses, sampled every 10 s.
# Four consecutive samples span 30 s. No missing samples here.
times = list(range(0, 401, 10))
errors = [max(exp(-t/40), exp(-t/80)) for t in times]
ready = next((times[i] for i in range(3, len(times))
              if all(e <= 0.05 for e in errors[i-3:i+1])), None)
print(f"two-location confirmation = {ready} s")

出力は傾き2.00、切片−4.00、20 mLの指令12.00 s、40 sで76.00 mL、二地点の確認270 sとなる。40 sの76 mLも同じ合成式から作った保留例であり、実機の汎化性能を示す検証ではない。時定数80を120へ変えると、同じ条件の確認時刻は390 sになる。記録時間内で条件を満たさなければ None を返す。

次の投入を許可する条件へつなぐ

この計算を制御へ接続する際は、待機を単なるタイマーにしない。本記事の設計案では「投入前確認 → 一回の投入 → 混合・測定待ち → 再評価」を分ける。校正範囲外の指令、測定の欠落・古さ、循環停止、水位異常、想定時間内の未安定では再投入せず原因を確認する。欠測を前の値で埋めて連続安定と判定しない。

待機後にECが目標へ届かない場合も、個別成分が不足したと直ちには判断できない。根圏の記事のように、ECと組成の違いを保つ。次の段階は、実測ログで吐出予測と待機条件を評価したうえで、一回量・累積量・再試行の上限を持つ投入制御を検討することである。

CEAの投入制御 — 上限・測定待ち・異常停止を状態で分ける

測定値が低いだけで、追加を繰り返さない

ポンプ校正と混合待ちの記事では、投入量と観測値が落ち着く時刻を別々に確認した。次に必要なのは、「今、もう一度投入してよいか」を決める規則である。測定値がまだ低くても、混合待ちの途中、測定が古い、循環が停止している、といった状況では追加の根拠にならない。

本記事では、一回の投入と再評価を状態に分け、量・回数・測定条件を確認する教材用の設計を示す。数値と入力はすべて合成条件であり、実機の運転基準や肥料の推奨量ではない。Pythonは投入の許可判定だけを再現し、ポンプを操作しない。

投入量を決める計算と、投入を許可する判定

養液タンクの収支から候補量を計算しても、実行可能とは限らない。候補量が校正範囲内か、水位や循環が正常か、前回の操作を評価できる観測があるかを別途確認する。

UGAの肥料注入器ガイドは、定期的な校正と保守を説明している。OSUの養液管理資料は、攪拌して読みが安定してから確認する手順を示している。以下の状態分けと上限の組み合わせは、それらを踏まえた本記事の設計案であり、資料に記載された制御仕様ではない。

一回の投入を六つの状態で管理する

状態 すること 次へ進める条件
CHECK:投入前確認 新しい観測、候補量、残り予算、設備状態を確認 条件を満たせば投入枠を記録してDOSEへ。目標を満たしていればDONEへ
DOSE:一回の投入 一意な操作IDに対する指令を一回だけ実行 停止を確認したらWAITへ。時間超過や流量不一致ならFAULTへ
WAIT:混合・測定待ち 新しい投入を禁止し、停止後の観測を集める 混合・測定条件を満たせばREVIEWへ。未安定のまま期限を超えたらFAULTへ
REVIEW:再評価 投入履歴と新しい観測から次の候補を計算 目標達成ならDONE、追加の根拠があればCHECK、不整合ならFAULT
DONE:操作終了 出力停止を保ち、記録を閉じる 次の操作は新しい運転判断として開始
FAULT:異常保持 停止を要求し、再投入を禁止 原因・実投入量・設備状態を確認してから明示的に復旧

DOSEの停止指令を出したことと、実際の流れが止まったことは同じではない。ソフトウェアの状態名だけで弁やポンプの故障を防げるわけではなく、実装時には実出力の確認、機器側の停止機構、再起動時の扱いも設計する。

FAULTを「少し待てば自動的にCHECKへ戻る状態」にしない。止まった理由を取り除かずに再試行すると、同じ異常で投入を積み重ねる可能性がある。

上限は投入前に使い、実投入量とは別の帳簿にする

この教材では一回16〜20 mL、一つの操作セッションで合計50 mL、許可回数は最大3回とする。16 mLの下限は、前記事の合成校正式が10秒で16 mLとなる範囲に対応する。実設備にそのまま使う値ではない。

許可済みの量を B [mL]、候補量を d [mL] とすると、許可前に次を確認する。

16\le d\le20,\qquad B+d\le50,\qquad n<3

許可した時点で B\leftarrow B+d、n\leftarrow n+1 として記録し、その後に指令へ進む。この B は投入枠を消費した量であり、実測した吐出量そのものではない。指令直後に通信が切れ、実行できたか不明になっても、ゼロ投入と決めつけて枠を返さない。実測量と不明量は別欄に残す。

合成操作 候補量 判定後の消費枠 結果
1回目 16 mL 16 mL 許可
停止・混合・再評価後の2回目 16 mL 32 mL 許可
再評価後の3回目 18 mL 50 mL 許可
さらに4回目 16 mL 50 mL 回数上限で拒否

32 mL消費した時点で20 mLを要求すれば、回数が残っていても合計52 mLとなるため拒否する。上限へ収めるために候補を自動的に切り詰めるのではなく、観測と候補量の根拠を再検討する。

セッションとは、ここでは開始と終了を記録した一連の補正操作を指す。ループの一周、画面更新、通信再接続を新しいセッションにしない。実運用では時間窓やタンク交換など、予算を更新する条件を明示し、再起動しても記録を失わないようにする。このコードには永続化はない。

「新しい」「安定した」を別々に確認する

測定の鮮度は受信時刻だけでは決まらない。送信元の測定時刻、時刻同期の状態、操作との順序を確認する。ここでは換算済みの測定経過時間を age_s とし、0〜5秒のみを受け入れる。未来時刻で負になる入力も拒否する。

after_stop は前回の停止確認後の測定かを表す。最初の投入では、現在の投入前確認を始めた後に得た測定かを使う。stable は前記事のような連続した観測による判定を上流で済ませた結果である。一つの値が変わらないことを、そのまま安定と扱わない。欠測、センサー固着、地点間の不一致も確認する。

5秒は教材の仮定である。実際の測定周期や通信遅延に合わない値を置けば、必要な観測まで拒否する。逆に許容時間を長くするだけでは、古い観測の問題を解決できない。

Pythonで許可判定を試す

次を dosing_gate.py として保存し、python3 dosing_gate.py で実行する。標準ライブラリだけを使う。

from math import isfinite


def gate(state, dose_ml, charged_ml, attempts, age_s,
         after_stop, stable, circulation, level_ok):
    """Offline authorization check; no hardware commands or persistence."""
    if state != "CHECK":
        return "STATE"
    if not all(isfinite(x) for x in (dose_ml, charged_ml, age_s)):
        return "INVALID"
    if charged_ml < 0 or type(attempts) is not int or attempts < 0:
        return "INVALID"
    if not all(type(x) is bool for x in
               (after_stop, stable, circulation, level_ok)):
        return "INVALID"
    if not circulation or not level_ok:
        return "INTERLOCK"
    if not 0 <= age_s <= 5 or not after_stop:
        return "STALE"
    if not stable:
        return "UNSTABLE"
    if not 16 <= dose_ml <= 20:
        return "DOSE_RANGE"
    if attempts >= 3:
        return "ATTEMPTS"
    if charged_ml + dose_ml > 50:
        return "BUDGET"
    return "ALLOW"


if __name__ == "__main__":
    charged, attempts, state = 0.0, 0, "CHECK"
    for requested in (16, 16, 18, 16):
        result = gate(state, requested, charged, attempts, 1,
                      True, True, True, True)
        if result == "ALLOW":
            # Charge BEFORE a simulated command; never refund uncertainty.
            charged += requested
            attempts += 1
            state = "WAIT"
        print(f"{requested} mL: {result}; charged={charged:.0f} mL")
        # Synthetic successful stop/mixing/reassessment, not a timer.
        state = "CHECK"

最初の3回は ALLOW、4回目は ATTEMPTS となり、消費枠は16、32、50、50 mLとなる。ループ末尾でCHECKへ戻す部分は、停止・混合・再評価が成功したという合成入力である。実装時に固定時間の経過だけでこの行を実行してはいけない。

この関数は、数値型の引数と状態を受け取るオフライン教材である。通信データの型変換、量の計算、安定判定、実流量監視、状態遷移の実行は含まない。NaN・無限大・負の消費量・不正な回数は拒否するが、外部入力を受ける完成したAPIではない。

正常系より先に確かめる境界

入力・状況 この教材で確認する結果
WAIT中に新しい候補が来る STATE。消費枠を増やさず投入しない
経過時間6秒、または負の経過時間 STALE
停止前の観測、または未安定 STALE、またはUNSTABLE
循環停止・水位異常 INTERLOCK
一回15 mL、または21 mL DOSE_RANGE
32 mL消費済みで20 mLを要求 BUDGET
消費枠や候補量がNaN INVALID

実装では同じ操作IDの再送を二度実行しないことも検証する。予算更新と操作IDの保存は、競合する二つの要求が同じ残量を使えないよう一つの確定操作として扱う。途中で停止した場合には、保存された指令と現場の実行状態を照合してから再開する。

この段階で分かること

この例で確かめられるのは、与えた入力と上限に対して許可判定が意図どおりになることまでである。ECから個別成分の不足を推定したり、pH差から酸の量を決めたりはしない。成長・収量、実機での安全性、濃度追従の性能も検証していない。

ここまでで収支、吐出校正、混合確認、投入許可の役割がつながった。実測ログによる装置検証は残る。次はNFT・DWC・点滴など、栽培方式によってタンクと根へ水が届く経路がどう変わるかを整理する。

次に読む

EC・pHと成分量の違いを確認する。CEA計測・画像推定 — センサー配置と投影葉面積シリーズの次の内容へ進むCEA水路・多段送液 — NFT・DWC・点滴と揚程分配同じ分野を別の観点から読むCEAアクアポニックス — 魚・微生物・植物の水質境界