24 using clock = std::chrono::steady_clock;
25 static const clock::time_point t0 = clock::now();
26 return std::chrono::duration<double>(clock::now() - t0).count();
30std::optional<Eigen::VectorXd>
json_vec(
const nlohmann::json& j,
int n) {
31 if (!j.is_array() ||
static_cast<int>(j.size()) <
n)
34 for (
int i = 0; i <
n; ++i) {
35 if (!j[
static_cast<size_t>(i)].
is_number())
37 v(i) = j[
static_cast<size_t>(i)].get<double>();
43std::optional<Eigen::MatrixXd>
json_mat(
const nlohmann::json& j,
int n) {
46 if (
static_cast<int>(j.size()) ==
n && j[0].is_array()) {
47 Eigen::MatrixXd m(
n,
n);
48 for (
int r = 0;
r <
n; ++
r) {
49 if (!j[
static_cast<size_t>(
r)].
is_array() ||
50 static_cast<int>(j[
static_cast<size_t>(
r)].size()) <
n)
52 for (
int c = 0; c <
n; ++c) {
53 const auto& e = j[
static_cast<size_t>(
r)][
static_cast<size_t>(c)];
56 m(
r, c) = e.get<
double>();
61 if (
static_cast<int>(j.size()) >=
n *
n) {
62 Eigen::MatrixXd m(
n,
n);
63 for (
int r = 0;
r <
n; ++
r)
64 for (
int c = 0; c <
n; ++c) {
65 const auto& e = j[
static_cast<size_t>(
r *
n + c)];
68 m(
r, c) = e.get<
double>();
77 return !s.empty() && std::all_of(s.begin(), s.end(), [](
unsigned char c) {
78 return std::isdigit(c) != 0;
84 const char* s = std::getenv(
"CAL_ROBUST_PRIOR_MAX_PSI_ERR");
88 double v = std::strtod(s, &
end);
89 if (
end == s || !std::isfinite(v) || v <= 0.0)
100 for (
auto& [
id, st] : states) {
103 if (st.framework->max_abs_error_vs_factory(st.baseline) > lim) {
104 std::cerr <<
"[RobustCalibration] Sensor " <<
static_cast<int>(id)
105 <<
": loaded θ vs current factory cubic mismatch > " << lim
106 <<
" PSI (ADC grid) — re-seeding robust from factory baseline.\n";
107 st.framework->seed_from_factory_cubic(st.baseline);
122 state.baseline = baseline;
123 state.framework = std::make_unique<RobustCalibrationFramework>(
static_cast<int>(sensor_id));
124 state.framework->seed_from_factory_cubic(baseline);
126 const double prior_limit = robust_prior_max_psi_mismatch();
133 if (
state.framework->max_abs_error_vs_factory(baseline) > prior_limit) {
134 std::cerr <<
"[RobustCalibration] Discarding adjustments prior for sensor "
135 <<
static_cast<int>(sensor_id) <<
": max |robust-factory| > " << prior_limit
136 <<
" PSI on ADC grid — re-seeding from factory cubic.\n";
137 state.framework->seed_from_factory_cubic(baseline);
141 if (
state.framework->max_abs_error_vs_factory(baseline) > prior_limit) {
142 std::cerr <<
"[RobustCalibration] Discarding adjustments mean-only prior for sensor "
143 <<
static_cast<int>(sensor_id) <<
" — re-seeding from factory cubic.\n";
144 state.framework->seed_from_factory_cubic(baseline);
150 if (
state.framework->max_abs_error_vs_factory(baseline) > prior_limit) {
151 std::cerr <<
"[RobustCalibration] Discarding population prior for sensor "
152 <<
static_cast<int>(sensor_id) <<
" — re-seeding from factory cubic.\n";
153 state.framework->seed_from_factory_cubic(baseline);
159 double reference_pressure) {
161 auto it =
states_.find(sensor_id);
162 if (it ==
states_.end() || !it->second.framework)
165 pt.
adc_code =
static_cast<double>(adc_code);
169 it->second.framework->add_calibration_point(pt);
174 auto it =
states_.find(sensor_id);
175 if (it ==
states_.end() || !it->second.framework)
178 pt.
adc_code =
static_cast<double>(adc_code);
182 it->second.framework->add_calibration_point(pt);
187 auto it =
states_.find(sensor_id);
188 if (it ==
states_.end() || !it->second.framework)
190 return it->second.framework->predict_pressure_psi(
static_cast<double>(adc_code));
195 auto it =
states_.find(sensor_id);
196 return it !=
states_.end() && it->second.framework !=
nullptr;
201 auto it =
states_.find(sensor_id);
204 auto&
state = it->second;
205 state.framework = std::make_unique<RobustCalibrationFramework>(
static_cast<int>(sensor_id));
206 state.framework->seed_from_factory_cubic(
state.baseline);
213 state.baseline = baseline;
214 state.framework = std::make_unique<RobustCalibrationFramework>(
static_cast<int>(sensor_id));
215 state.framework->seed_from_factory_cubic(baseline);
219 const std::string&
path,
const std::map<uint16_t, std::string>* uid_to_role)
const {
222 nlohmann::json fw2 = nlohmann::json::object();
224 if (!
state.framework)
226 Eigen::VectorXd t =
state.framework->theta_mean();
227 Eigen::MatrixXd cov =
state.framework->theta_cov();
229 for (
int i = 0; i < N; ++i)
230 mean.push_back(t(i));
232 for (
int r = 0;
r < N; ++
r) {
234 for (
int c = 0; c < N; ++c)
235 row.push_back(cov(
r, c));
238 nlohmann::json entry = {{
"theta_mean", mean}, {
"theta_cov", cvec}};
241 auto rit = uid_to_role->find(
id);
242 if (rit != uid_to_role->end() && !rit->second.empty())
243 entry[
"role"] = rit->second;
245 fw2[std::to_string(
static_cast<int>(
id))] = std::move(entry);
247 const nlohmann::json root = {{
"framework_v2", fw2}};
249 std::ofstream file(
path);
252 file << root.dump(2) <<
"\n";
257 const std::string&
path,
const std::map<uint16_t, std::string>* uid_to_role) {
259 std::ifstream file(
path);
262 std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
272 root = nlohmann::json::parse(content);
276 if (!root.is_object())
283 std::map<std::string, uint16_t> role_to_uid;
285 for (
const auto& [uid, role] : *uid_to_role)
287 role_to_uid.emplace(role, uid);
289 auto apply_restored = [&]() {
291 if (!
state.framework)
306 if (root.contains(
"framework_v2") && root[
"framework_v2"].is_object()) {
307 for (
auto& [id_str, sub] : root[
"framework_v2"].items()) {
308 if (!is_numeric_key(id_str) || !sub.is_object())
310 uint16_t
id =
static_cast<uint16_t
>(std::stoi(id_str));
313 if (sub.contains(
"role") && sub[
"role"].is_string()) {
314 auto rit = role_to_uid.find(sub[
"role"].get<std::string>());
315 if (rit != role_to_uid.end())
318 auto mean = sub.contains(
"theta_mean") ? json_vec(sub[
"theta_mean"], N) : std::nullopt;
319 auto cov = sub.contains(
"theta_cov") ? json_mat(sub[
"theta_cov"], N) : std::nullopt;
326 reconcile_frameworks_with_factory_baseline(
states_);
331 if (root.contains(
"framework_v1") && root[
"framework_v1"].is_object()) {
332 for (
auto& [id_str, val] : root[
"framework_v1"].items()) {
333 if (!is_numeric_key(id_str))
335 const uint16_t
id =
static_cast<uint16_t
>(std::stoi(id_str));
336 const nlohmann::json& arr =
338 : (val.is_object() && val.contains(
"theta_mean") ? val[
"theta_mean"]
340 auto mean = json_vec(arr, N);
344 if (it !=
states_.end() && it->second.framework)
345 it->second.framework->set_theta_mean_for_restore(*mean);
348 reconcile_frameworks_with_factory_baseline(
states_);
353 if (root.contains(
"population_prior") && root.contains(
"pt_states")) {
354 if (root[
"population_prior"].is_object()) {
355 const auto& pp = root[
"population_prior"];
357 pp.contains(
"population_mean") ? json_vec(pp[
"population_mean"], N) : std::nullopt;
358 auto pc = pp.contains(
"population_covariance")
359 ? json_mat(pp[
"population_covariance"], N)
366 if (root[
"pt_states"].is_object()) {
367 for (
auto& [id_str, sub] : root[
"pt_states"].items()) {
368 if (!is_numeric_key(id_str) || !sub.is_object())
370 const uint16_t
id =
static_cast<uint16_t
>(std::stoi(id_str));
372 sub.contains(
"theta_mean") ? json_vec(sub[
"theta_mean"], N) : std::nullopt;
373 auto cov = sub.contains(
"theta_cov") ? json_mat(sub[
"theta_cov"], N) : std::nullopt;
381 reconcile_frameworks_with_factory_baseline(
states_);
* array(stored with pointer to save storage) array_t *array
void update_calibration(uint16_t sensor_id, int32_t adc_code, double reference_pressure)
bool load_adjustments(const std::string &path, const std::map< uint16_t, std::string > *uid_to_role=nullptr)
Restore per-sensor θ.
bool save_adjustments(const std::string &path, const std::map< uint16_t, std::string > *uid_to_role=nullptr) const
Persist per-sensor θ.
void zero_sensor(uint16_t sensor_id, int32_t adc_code)
std::map< uint16_t, Eigen::VectorXd > restored_theta_mean_
void reseed_sensor(uint16_t sensor_id, const PTCalibrationCoeffs &baseline)
Reseed a sensor's framework cleanly from the given cubic baseline, discarding any learned adjustments...
std::optional< Eigen::VectorXd > population_theta_mean_
void reset_adjustment(uint16_t sensor_id)
void initialize_sensor(uint16_t sensor_id, const PTCalibrationCoeffs &baseline)
std::optional< Eigen::MatrixXd > population_theta_cov_
double predict_pressure_psi(uint16_t sensor_id, int32_t adc_code)
Mean PSI from the robust model (default environment).
RobustCalibrationManager()
std::map< uint16_t, Eigen::MatrixXd > restored_theta_cov_
std::map< uint16_t, SensorState > states_
bool has_sensor(uint16_t sensor_id) const
True if this sensor_id was initialized for robust tracking (factory-baselined).
static ActuatorControllerState state
*brief create JSON pointer *sa reference_tokens end()
double robust_prior_max_psi_mismatch()
Saved θ that drifts far from the factory cubic is usually bad priors — drop them.
void reconcile_frameworks_with_factory_baseline(std::map< uint16_t, SensorState > &states)
After loading adjustments.json, θ may still match an old factory seed (e.g.
double monotonic_sec_now()
std::optional< Eigen::VectorXd > json_vec(const nlohmann::json &j, int n)
bool is_numeric_key(const std::string &s)
std::optional< Eigen::MatrixXd > json_mat(const nlohmann::json &j, int n)
Output smoothing for calibrated PT pressure, per sensor uid (ESP32 and LabJack T7 alike).
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
PT Calibration Coefficients.