///|
pub(all) enum SoilClass {
Sand
SandyLoam
Loam
ClayLoam
Clay
UrbanFill
EngineeredMedia
} derive(Eq, Debug)
///|
pub(all) struct SoilProfile {
kind : SoilClass
name : String
infiltration_rate_mm_per_hr : Double
porosity : Double
runoff_adjustment : Double
clogging_risk : Double
maintenance_interval_days : Int
} derive(Eq, Debug)
///|
pub(all) enum MaintenancePriority {
Routine
Watch
ServiceSoon
Restore
} derive(Eq, Debug)
///|
pub(all) enum MaintenanceAction {
InspectInlet
RemoveSediment
RenewMulch
CheckOverflow
ReplantBareSoil
LoosenSurface
} derive(Eq, Debug)
///|
pub(all) struct MaintenancePlan {
priority : MaintenancePriority
interval_days : Int
risk_score : Int
actions : Array[MaintenanceAction]
note : String
} derive(Eq, Debug)
///|
pub fn soil_profile(kind : SoilClass) -> SoilProfile {
match kind {
Sand => profile(kind, "sand", 75.0, 0.35, 0.85, 0.20, 120)
SandyLoam => profile(kind, "sandy loam", 45.0, 0.32, 0.90, 0.30, 90)
Loam => profile(kind, "loam", 25.0, 0.28, 1.00, 0.45, 75)
ClayLoam => profile(kind, "clay loam", 12.0, 0.24, 1.10, 0.65, 45)
Clay => profile(kind, "clay", 5.0, 0.18, 1.20, 0.85, 30)
UrbanFill => profile(kind, "urban fill", 8.0, 0.20, 1.15, 0.75, 30)
EngineeredMedia =>
profile(kind, "engineered media", 50.0, 0.30, 0.95, 0.25, 120)
}
}
///|
pub fn SoilClass::label(self : SoilClass) -> String {
match self {
Sand => "sand"
SandyLoam => "sandy_loam"
Loam => "loam"
ClayLoam => "clay_loam"
Clay => "clay"
UrbanFill => "urban_fill"
EngineeredMedia => "engineered_media"
}
}
///|
pub fn MaintenancePriority::label(self : MaintenancePriority) -> String {
match self {
Routine => "routine"
Watch => "watch"
ServiceSoon => "service_soon"
Restore => "restore"
}
}
///|
pub fn MaintenanceAction::label(self : MaintenanceAction) -> String {
match self {
InspectInlet => "inspect_inlet"
RemoveSediment => "remove_sediment"
RenewMulch => "renew_mulch"
CheckOverflow => "check_overflow"
ReplantBareSoil => "replant_bare_soil"
LoosenSurface => "loosen_surface"
}
}
///|
pub fn spec_with_soil(
spec : GardenSpec,
soil : SoilProfile,
) -> Result[GardenSpec, SpongeError] {
match validate_soil(soil) {
Some(error) => Err(error)
None =>
Ok({
..spec,
media_porosity: soil.porosity,
infiltration_rate_mm_per_hr: soil.infiltration_rate_mm_per_hr,
})
}
}
///|
pub fn adjusted_catchment_for_soil(
catchment : Catchment,
soil : SoilProfile,
) -> Result[Catchment, SpongeError] {
match validate_soil(soil) {
Some(error) => Err(error)
None => {
let adjusted = clamp_double(
catchment.runoff_coefficient * soil.runoff_adjustment,
0.0,
1.0,
)
Ok({ ..catchment, runoff_coefficient: adjusted })
}
}
}
///|
pub fn blended_soil_profile(
name : String,
soils : Array[SoilProfile],
weights : Array[Double],
) -> Result[SoilProfile, SpongeError] {
if soils.length() != weights.length() || soils.length() == 0 {
return Err(InvalidWeight)
}
let mut total_weight = 0.0
let mut infiltration = 0.0
let mut porosity = 0.0
let mut runoff_adjustment = 0.0
let mut clogging_risk = 0.0
for index, soil in soils {
match validate_soil(soil) {
Some(error) => return Err(error)
None => ()
}
let weight = weights[index]
if weight < 0.0 {
return Err(InvalidWeight)
}
total_weight = total_weight + weight
infiltration = infiltration + soil.infiltration_rate_mm_per_hr * weight
porosity = porosity + soil.porosity * weight
runoff_adjustment = runoff_adjustment + soil.runoff_adjustment * weight
clogging_risk = clogging_risk + soil.clogging_risk * weight
}
if total_weight <= 0.0 {
return Err(InvalidWeight)
}
let risk = clogging_risk / total_weight
Ok({
kind: EngineeredMedia,
name,
infiltration_rate_mm_per_hr: infiltration / total_weight,
porosity: porosity / total_weight,
runoff_adjustment: runoff_adjustment / total_weight,
clogging_risk: risk,
maintenance_interval_days: interval_for_risk(risk),
})
}
///|
pub fn maintenance_plan(
plan : GardenPlan,
soil : SoilProfile,
sediment_load_index : Double,
months_since_service : Int,
) -> Result[MaintenancePlan, SpongeError] {
match validate_soil(soil) {
Some(error) => Err(error)
None =>
if sediment_load_index < 0.0 || sediment_load_index > 1.0 {
Err(InvalidSedimentLoad)
} else {
let risk = maintenance_risk_score(
plan, soil, sediment_load_index, months_since_service,
)
let priority = priority_for_risk(risk)
Ok({
priority,
interval_days: soil.maintenance_interval_days,
risk_score: risk,
actions: actions_for_priority(priority, sediment_load_index, plan),
note: note_for_priority(priority),
})
}
}
}
///|
pub fn SoilProfile::to_json(self : SoilProfile) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"kind\":\"" + self.kind.label() + "\",")
out.write_string("\"name\":\"" + escape_json(self.name) + "\",")
out.write_string(
"\"infiltration_rate_mm_per_hr\":" +
self.infiltration_rate_mm_per_hr.to_string() +
",",
)
out.write_string("\"porosity\":" + self.porosity.to_string() + ",")
out.write_string(
"\"runoff_adjustment\":" + self.runoff_adjustment.to_string() + ",",
)
out.write_string("\"clogging_risk\":" + self.clogging_risk.to_string() + ",")
out.write_string(
"\"maintenance_interval_days\":" +
self.maintenance_interval_days.to_string(),
)
out.write_string("}")
out.to_string()
}
///|
pub fn MaintenancePlan::to_json(self : MaintenancePlan) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"priority\":\"" + self.priority.label() + "\",")
out.write_string("\"interval_days\":" + self.interval_days.to_string() + ",")
out.write_string("\"risk_score\":" + self.risk_score.to_string() + ",")
out.write_string("\"actions\":[")
for index, action in self.actions {
if index > 0 {
out.write_string(",")
}
out.write_string("\"" + action.label() + "\"")
}
out.write_string("],\"note\":\"" + escape_json(self.note) + "\"}")
out.to_string()
}
///|
fn profile(
kind : SoilClass,
name : String,
infiltration_rate_mm_per_hr : Double,
porosity : Double,
runoff_adjustment : Double,
clogging_risk : Double,
maintenance_interval_days : Int,
) -> SoilProfile {
{
kind,
name,
infiltration_rate_mm_per_hr,
porosity,
runoff_adjustment,
clogging_risk,
maintenance_interval_days,
}
}
///|
fn validate_soil(soil : SoilProfile) -> SpongeError? {
if soil.infiltration_rate_mm_per_hr <= 0.0 {
Some(InvalidInfiltrationRate)
} else if soil.porosity < 0.0 || soil.porosity > 0.8 {
Some(InvalidStorageDepth)
} else if soil.runoff_adjustment < 0.0 {
Some(InvalidRunoffCoefficient)
} else if soil.clogging_risk < 0.0 || soil.clogging_risk > 1.0 {
Some(InvalidSedimentLoad)
} else {
None
}
}
///|
fn maintenance_risk_score(
plan : GardenPlan,
soil : SoilProfile,
sediment_load_index : Double,
months_since_service : Int,
) -> Int {
let service_age = if months_since_service < 0 {
0
} else {
months_since_service
}
let drawdown_risk = if plan.drawdown_hours > 48.0 {
30
} else if plan.drawdown_hours > 24.0 {
18
} else if plan.drawdown_hours > 12.0 {
8
} else {
0
}
let capture_risk = if plan.capture_ratio < 0.70 {
20
} else if plan.capture_ratio < 0.90 {
10
} else {
0
}
let soil_risk = (soil.clogging_risk * 30.0).to_int()
let sediment_risk = (sediment_load_index * 25.0).to_int()
let age_risk = if service_age >= 12 {
15
} else if service_age >= 6 {
8
} else {
0
}
clamp_int(
drawdown_risk + capture_risk + soil_risk + sediment_risk + age_risk,
0,
100,
)
}
///|
fn priority_for_risk(risk : Int) -> MaintenancePriority {
if risk >= 75 {
Restore
} else if risk >= 50 {
ServiceSoon
} else if risk >= 25 {
Watch
} else {
Routine
}
}
///|
fn actions_for_priority(
priority : MaintenancePriority,
sediment_load_index : Double,
plan : GardenPlan,
) -> Array[MaintenanceAction] {
let actions : Array[MaintenanceAction] = [InspectInlet, CheckOverflow]
if priority == Watch || priority == ServiceSoon || priority == Restore {
actions.push(RemoveSediment)
}
if sediment_load_index >= 0.50 {
actions.push(RenewMulch)
}
if plan.drawdown_hours > 24.0 {
actions.push(LoosenSurface)
}
if priority == Restore {
actions.push(ReplantBareSoil)
}
actions
}
///|
fn note_for_priority(priority : MaintenancePriority) -> String {
match priority {
Routine => "routine inspection is enough for the current design card"
Watch => "watch inlet sediment and repeat drawdown observation after storms"
ServiceSoon => "schedule sediment removal and surface maintenance soon"
Restore =>
"restore media surface and vegetation before relying on capture results"
}
}
///|
fn interval_for_risk(risk : Double) -> Int {
if risk >= 0.75 {
30
} else if risk >= 0.55 {
45
} else if risk >= 0.35 {
75
} else {
120
}
}
///|
fn clamp_double(value : Double, low : Double, high : Double) -> Double {
if value < low {
low
} else if value > high {
high
} else {
value
}
}
///|
fn clamp_int(value : Int, low : Int, high : Int) -> Int {
if value < low {
low
} else if value > high {
high
} else {
value
}
}