"""Synthetic carbon accounting and dilute water-vapor transfer. Python >= 3.10. Not a fitted crop model. Carbon uses gC/h and hours. Water uses total conductance per consistently defined leaf area; both pressures use kPa. No dependencies or file writes. See the accompanying README and article. """ import argparse import json import math from pathlib import Path import unittest def nonnegative(*values): if any(isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or v < 0 for v in values): raise ValueError("Inputs must be finite nonnegative numbers") def carbon(uptake_gc_h, release_gc_h, hours): nonnegative(uptake_gc_h, release_gc_h, hours) return (uptake_gc_h - release_gc_h) * hours def water(total_conductance, leaf_air_kpa, pressure_kpa=100.0): nonnegative(total_conductance, leaf_air_kpa, pressure_kpa) if pressure_kpa <= 0 or leaf_air_kpa >= pressure_kpa: raise ValueError("Require 0 <= vapor difference < positive total pressure") # Positive outward transfer only; condensation is outside this example. return total_conductance * (leaf_air_kpa / pressure_kpa) * 0.018 * 3600 def verify(actual, expected): if type(actual) is not type(expected): raise ValueError("Result type mismatch") if isinstance(actual, dict): if actual.keys() != expected.keys(): raise ValueError("Result keys differ") for key in actual: verify(actual[key], expected[key]) elif isinstance(actual, (float, int)): if not (math.isfinite(actual) and math.isfinite(expected) and math.isclose(actual, expected, rel_tol=1e-10, abs_tol=1e-10)): raise ValueError("Numerical result differs") elif actual != expected: raise ValueError("Result differs") class Checks(unittest.TestCase): def test_dark_carbon_loss(self): self.assertAlmostEqual(carbon(0, 0.15, 8), -1.2) def test_period_partition(self): self.assertAlmostEqual(carbon(0.8, 0.15, 16), sum(carbon(0.8, 0.15, 1) for _ in range(16))) def test_release_sensitivity(self): baseline = carbon(0.8, 0.15, 16) + carbon(0, 0.15, 8) changed = carbon(0.8, 0.25, 16) + carbon(0, 0.25, 8) self.assertAlmostEqual(baseline - changed, 2.4) def test_water_units(self): # 2 mmol/(m2 s) * 18 mg/mmol * 3600 s/h = 129600 mg/(m2 h) self.assertAlmostEqual(water(0.2, 1), 129600 / 1e6) def test_zero_transfer(self): self.assertEqual(water(0, 1), 0) self.assertEqual(water(0.2, 0), 0) def test_same_flux_different_inputs(self): self.assertAlmostEqual(water(0.2, 1), water(0.1, 2)) def test_invalid_inputs(self): for bad in (-1, float('nan'), float('inf'), True, '1'): with self.subTest(bad=bad): with self.assertRaises(ValueError): carbon(bad, 0.15, 16) with self.assertRaises(ValueError): water(0.2, bad) for pressure in (0, 0.5): with self.assertRaises(ValueError): water(0.2, 1, pressure) def test_verification_rejects_bad_results(self): for bad in ({'x': float('nan')}, {'x': 2.0}, {}, {'x': 1.0, 'y': 2}): with self.assertRaises(ValueError): verify({'x': 1.0}, bad) def examples(): return { 'scope': 'synthetic; carbon change is not yield; prescribed conductance', 'carbon_gc': { 'light': carbon(0.8, 0.15, 16), 'dark': carbon(0, 0.15, 8), 'daily': carbon(0.8, 0.15, 16) + carbon(0, 0.15, 8), 'higher_release_daily': carbon(0.8, 0.25, 16) + carbon(0, 0.25, 8), }, 'water_kg_per_m2_leaf_per_h': { 'A': water(0.2, 1), 'B': water(0.1, 1), 'C': water(0.1, 2), }, } if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) group = parser.add_mutually_exclusive_group() group.add_argument('--self-test', action='store_true') group.add_argument('--verify', type=Path) args = parser.parse_args() if args.self_test: suite = unittest.defaultTestLoader.loadTestsFromTestCase(Checks) result = unittest.TextTestRunner(verbosity=2).run(suite) raise SystemExit(0 if result.wasSuccessful() else 1) if args.verify: verify(examples(), json.loads(args.verify.read_text())) print('All expected results verified') else: print(json.dumps(examples(), indent=2, allow_nan=False))