SedSat3 1.1.6
Sediment Source Apportionment Tool - Advanced statistical methods for environmental pollution research
Loading...
Searching...
No Matches
sourcesinkdata.cpp
Go to the documentation of this file.
1#include "sourcesinkdata.h"
2#include "iostream"
3#include "NormalDist.h"
4#include "qjsondocument.h"
5#include "resultitem.h"
6#include <gsl/gsl_cdf.h>
7#include "GADistribution.h"
8#include "rangeset.h"
9#include "QJsonArray"
10#include "QJsonValue"
11#include <qdir.h>
12
13
14
15// ========== Construction and Assignment ==========
16
19 element_information_(),
20 element_distributions_(),
21 numberofconstituents_(0),
22 numberofisotopes_(0),
23 numberofsourcesamplesets_(0),
24 observations_(),
25 outputpath_(),
26 parameters_(),
27 target_group_(),
28 samplesetsorder_(),
29 constituent_order_(),
30 selected_target_sample_(),
31 element_order_(),
32 isotope_order_(),
33 size_om_order_(),
34 parameter_estimation_mode_(estimation_mode::elemental_profile_and_contribution),
35 omconstituent_(),
36 sizeconsituent_(),
37 regression_p_value_threshold_(0.05),
38 distance_coeff_(1.0),
39 tools_used_(),
40 options_()
41{
42 options_["Outlier deviation threshold"] = 3.0;
43 rtw_ = nullptr;
44}
45
47 : map<string, Elemental_Profile_Set>(other),
48 element_information_(other.element_information_),
49 element_distributions_(other.element_distributions_),
50 numberofconstituents_(other.numberofconstituents_),
51 numberofisotopes_(other.numberofisotopes_),
52 numberofsourcesamplesets_(other.numberofsourcesamplesets_),
53 observations_(other.observations_),
54 outputpath_(other.outputpath_),
55 parameters_(other.parameters_),
56 target_group_(other.target_group_),
57 samplesetsorder_(other.samplesetsorder_),
58 constituent_order_(other.constituent_order_),
59 selected_target_sample_(other.selected_target_sample_),
60 element_order_(other.element_order_),
61 isotope_order_(other.isotope_order_),
62 size_om_order_(other.size_om_order_),
63 parameter_estimation_mode_(other.parameter_estimation_mode_),
64 omconstituent_(other.omconstituent_),
65 sizeconsituent_(other.sizeconsituent_),
66 regression_p_value_threshold_(other.regression_p_value_threshold_),
67 distance_coeff_(other.distance_coeff_),
68 tools_used_(other.tools_used_),
69 options_(other.options_)
70{
71 rtw_ = nullptr;
72}
73
75{
76 // Check for self-assignment
77 if (this == &other) {
78 return *this;
79 }
80
81 // Copy base class
82 map<string, Elemental_Profile_Set>::operator=(other);
83
84 // Copy all member variables
105 tools_used_ = other.tools_used_;
106 options_ = other.options_;
107
108 return *this;
109}
110
112 const string& target,
113 bool omnsizecorrect,
114 map<string, element_information>* elementinfo)
115{
116 SourceSinkData corrected;
118
119 // Build OM and particle size vector from target sample
120 vector<double> om_size;
121 if (!omconstituent_.empty()) {
122 om_size.push_back(at(target_group_).GetProfile(selected_target_sample_)->at(omconstituent_));
123 }
124 if (!sizeconsituent_.empty()) {
125 om_size.push_back(at(target_group_).GetProfile(selected_target_sample_)->at(sizeconsituent_));
126 }
127
128 // Copy profile sets with appropriate corrections
129 for (auto& [group_name, profile_set] : *this)
130 {
131 // Apply OM/size corrections to source groups, but not to target group
132 bool apply_corrections = (group_name != target_group_) && omnsizecorrect;
133 corrected[group_name] = profile_set.CopyIncludedInAnalysis(
134 apply_corrections,
135 om_size,
136 elementinfo
137 );
138 }
139
140 // Copy analysis settings
141 corrected.omconstituent_ = omconstituent_;
144
145 // Copy element information and distributions
146 if (elementinfo)
147 {
148 // Filter to only include elements marked for analysis
149 for (const auto& [element_name, elem_info] : element_information_)
150 {
151 if (elem_info.include_in_analysis &&
155 {
156 corrected.element_information_[element_name] = element_information_[element_name];
157 corrected.element_distributions_[element_name] = element_distributions_[element_name];
158 }
159 }
160 }
161 else
162 {
163 // Copy all element information
166 }
167
168 // Copy metadata and ordering
172 corrected.observations_ = observations_;
173 corrected.outputpath_ = outputpath_;
174 corrected.target_group_ = target_group_;
178 corrected.element_order_ = element_order_;
179 corrected.isotope_order_ = isotope_order_;
180 corrected.size_om_order_ = size_om_order_;
182 corrected.options_ = options_;
183
184 // Populate distributions for the corrected dataset
186 corrected.AssignAllDistributions();
187
188 return corrected;
189}
190
191int SourceSinkData::CountElements(bool exclude_elements) const
192{
193 if (!exclude_elements) {
194 return element_information_.size();
195 }
196
197 int count = 0;
198 for (const auto& [element_name, elem_info] : element_information_)
199 {
200 // Include if marked for analysis AND not an excluded role
201 if (elem_info.include_in_analysis &&
205 {
206 count++;
207 }
208 }
209
210 return count;
211}
212
214 bool exclude_samples,
215 bool exclude_elements,
216 bool omnsizecorrect,
217 const string& target) const
218{
219 SourceSinkData corrected;
220 corrected.target_group_ = target_group_;
221
222 // Set target sample (use provided or keep current)
223 if (!target.empty()) {
224 corrected.selected_target_sample_ = target;
225 }
226 else {
228 }
229
230 // Build OM and particle size vector from target sample (if corrections enabled)
231 vector<double> om_size;
232 if (omnsizecorrect)
233 {
234 if (!omconstituent_.empty()) {
235 om_size.push_back(
236 at(target_group_).GetProfile(corrected.selected_target_sample_).at(omconstituent_)
237 );
238 }
239 if (!sizeconsituent_.empty()) {
240 om_size.push_back(
241 at(target_group_).GetProfile(corrected.selected_target_sample_).at(sizeconsituent_)
242 );
243 }
244 }
245
246 // Copy and correct each profile set
247 for (const auto& [group_name, profile_set] : *this)
248 {
249 if (group_name == target_group_)
250 {
251 // Target group: apply element filtering but no sample filtering or corrections
252 corrected[group_name] = profile_set.CreateCorrectedSet(
253 false, // Don't exclude samples from target
254 exclude_elements, // Apply element filtering
255 false, // Don't apply OM/size corrections to target
256 om_size,
258 );
259 }
260 else
261 {
262 // Source groups: apply all requested filtering and corrections
263 corrected[group_name] = profile_set.CreateCorrectedSet(
264 exclude_samples, // Apply sample filtering
265 exclude_elements, // Apply element filtering
266 omnsizecorrect, // Apply OM/size corrections
267 om_size,
269 );
270 }
271 }
272
273 // Copy settings and metadata
274 corrected.omconstituent_ = omconstituent_;
277 corrected.options_ = options_;
278
279 // Populate element information and distributions
282 corrected.AssignAllDistributions();
283
284 return corrected;
285}
286
288{
289 SourceSinkData extracted;
290 extracted.target_group_ = target_group_;
291
292 // Extract chemical elements from each profile set
293 for (const auto& [group_name, profile_set] : *this)
294 {
295 extracted[group_name] = profile_set.ExtractChemicalElements(&element_information_, isotopes);
296 }
297
298 // Copy settings
299 extracted.omconstituent_ = omconstituent_;
302
303 // Populate distributions for extracted elements
306 extracted.AssignAllDistributions();
307
308 return extracted;
309}
310
311SourceSinkData SourceSinkData::ExtractSpecificElements(const vector<string>& element_list) const
312{
313 SourceSinkData extracted;
314
315 // Extract specified elements from source groups only (not target)
316 for (const auto& [group_name, profile_set] : *this)
317 {
318 if (group_name != target_group_)
319 {
320 extracted[group_name] = profile_set.ExtractElements(element_list);
321 }
322 }
323
324 // Populate distributions for extracted elements
327 extracted.AssignAllDistributions();
328
329 return extracted;
330}
331
333{
334 // Clear all profile sets (base class map)
335 clear();
336
337 // Clear element data
338 element_information_.clear();
340
341 // Reset counters
345
346 // Clear MCMC/optimization data
347 observations_.clear();
348 parameters_.clear();
349
350 // Clear identifiers and paths
351 outputpath_.clear();
352 target_group_.clear();
354
355 // Clear ordering vectors
356 samplesetsorder_.clear();
357 constituent_order_.clear();
358 element_order_.clear();
359 isotope_order_.clear();
360 size_om_order_.clear();
361
362 // Clear tracking data
363 tools_used_.clear();
364
365 // Note: options_ is intentionally not cleared (preserves user settings)
366}
367
369 const string& name,
371{
372 // Check if group name already exists
373 if (count(name) > 0)
374 {
375 std::cerr << "Sample set '" + name + "' already exists!" << std::endl;
376 return nullptr;
377 }
378
379 // Add the new sample set
380 operator[](name) = elemental_profile_set;
381
382 return &operator[](name);
383}
384
385
387{
388 if (count(name) == 0) {
389 return nullptr;
390 }
391
392 return &operator[](name);
393}
394
395vector<string> SourceSinkData::GetSampleNames(const string& group_name) const
396{
397 const Elemental_Profile_Set* profile_set =
398 (count(group_name) > 0) ? &at(group_name) : nullptr;
399
400 if (profile_set) {
401 return profile_set->GetSampleNames();
402 }
403
404 return vector<string>();
405}
406
407vector<string> SourceSinkData::GetGroupNames() const
408{
409 vector<string> group_names;
410 group_names.reserve(size());
411
412 for (const auto& [group_name, profile_set] : *this)
413 {
414 group_names.push_back(group_name);
415 }
416
417 return group_names;
418}
419
421{
422 if (empty()) {
423 return vector<string>();
424 }
425
426 // Get element names from first sample of first group
427 const Elemental_Profile_Set& first_group = begin()->second;
428 if (first_group.empty()) {
429 return vector<string>();
430 }
431
432 const Elemental_Profile& first_sample = first_group.begin()->second;
433
434 vector<string> element_names;
435 element_names.reserve(first_sample.size());
436
437 for (const auto& [element_name, concentration] : first_sample)
438 {
439 element_names.push_back(element_name);
440 }
441
442 return element_names;
443}
444
446 const vector<vector<string>>& indicators) const
447{
448 profiles_data extracted;
449 extracted.element_names = GetElementNames();
450 extracted.values.reserve(indicators.size());
451 extracted.sample_names.reserve(indicators.size());
452
453 for (const auto& indicator : indicators)
454 {
455 const string& group_name = indicator[0];
456 const string& sample_name = indicator[1];
457
458 const Elemental_Profile_Set* group =
459 (count(group_name) > 0) ? &at(group_name) : nullptr;
460
461 if (group)
462 {
463 vector<double> concentrations = group->GetConcentrationsForSample(sample_name);
464 extracted.values.push_back(concentrations);
465 extracted.sample_names.push_back(sample_name);
466 }
467 }
468
469 return extracted;
470}
471
473 const vector<vector<string>>& indicators) const
474{
475 Elemental_Profile_Set extracted;
476
477 for (const auto& indicator : indicators)
478 {
479 const string& group_name = indicator[0];
480 const string& sample_name = indicator[1];
481
482 const Elemental_Profile_Set* group =
483 (count(group_name) > 0) ? &at(group_name) : nullptr;
484
485 if (group && group->count(sample_name) > 0)
486 {
487 const Elemental_Profile& profile = group->at(sample_name);
488 // Prefix with group name to ensure uniqueness
489 extracted.AppendProfile(group_name + "-" + sample_name, profile);
490 }
491 }
492
493 return extracted;
494}
496 const string& element,
497 const string& group) const
498{
499 element_data extracted;
500 extracted.group_name = group;
501
502 const Elemental_Profile_Set* profile_set =
503 (count(group) > 0) ? &at(group) : nullptr;
504
505 if (!profile_set) {
506 return extracted;
507 }
508
509 extracted.values.reserve(profile_set->size());
510 extracted.sample_names.reserve(profile_set->size());
511
512 for (const auto& [sample_name, profile] : *profile_set)
513 {
514 extracted.values.push_back(profile.GetValue(element));
515 extracted.sample_names.push_back(sample_name);
516 }
517
518 return extracted;
519}
520
522{
523 vector<string> element_names = GetElementNames();
525
526 // Update distributions within each group, then aggregate
527 for (auto& [group_name, profile_set] : *this)
528 {
529 // Build distributions within this group
530 profile_set.UpdateElementDistributions();
531
532 // Aggregate into overall distributions
533 for (const string& element : element_names)
534 {
535 element_distributions_[element].AppendSet(
536 profile_set.GetElementDistribution(element)
537 );
538 }
539 }
540}
541
542map<string, vector<double>> SourceSinkData::ExtractElementDataByGroup(const string& element) const
543{
544 map<string, vector<double>> extracted;
545 vector<string> group_names = GetGroupNames();
546
547 for (const string& group_name : group_names)
548 {
549 element_data group_data = ExtractElementConcentrations(element, group_name);
550 extracted[group_name] = group_data.values;
551 }
552
553 return extracted;
554}
555
556
558{
559 vector<string> element_names = GetElementNames();
560
561 for (const string& element : element_names)
562 {
563 // Assign distribution at dataset level (all groups combined)
564 Distribution* overall_fitted = element_distributions_[element].GetFittedDistribution();
565 overall_fitted->distribution = element_distributions_[element].SelectBestDistribution();
566 overall_fitted->parameters = element_distributions_[element].EstimateDistributionParameters();
567
568 // Assign distributions at group level
569 for (auto& [group_name, profile_set] : *this)
570 {
571 ConcentrationSet* group_dist = profile_set.GetElementDistribution(element);
572 Distribution* group_fitted = group_dist->GetFittedDistribution();
573
574 // Use same distribution type as overall
575 group_fitted->distribution = overall_fitted->distribution;
576
577 // Estimate parameters from group-specific data
579 {
580 // Elements: use selected distribution type
581 group_fitted->parameters = group_dist->EstimateDistributionParameters(
582 overall_fitted->distribution
583 );
584 }
585 else
586 {
587 // Isotopes: always use normal distribution
588 group_fitted->parameters = group_dist->EstimateDistributionParameters(
590 );
591 }
592 }
593 }
594}
595
597{
598 if (element_distributions_.count(element_name) == 0) {
599 return nullptr;
600 }
601
602 return element_distributions_[element_name].GetFittedDistribution();
603}
604
605void SourceSinkData::PopulateElementInformation(const map<string, element_information>* ElementInfo)
606{
607 element_information_.clear();
608 vector<string> element_names = GetElementNames();
609
610 for (const string& element : element_names)
611 {
612 if (ElementInfo == nullptr)
613 {
614 // Use default element information
616 }
617 else
618 {
619 // Copy from provided element information
620 element_information_[element] = ElementInfo->at(element);
621 }
622 }
623
624 // Update source sample set count
625 if (!target_group_.empty())
626 {
627 numberofsourcesamplesets_ = size() - 1; // Exclude target group
628 }
629 else
630 {
631 numberofsourcesamplesets_ = size(); // No target group
632 }
633}
634
635string SourceSinkData::GetParameterName(int index) const
636{
637 if (parameter(index))
638 {
639 return parameter(index)->Name();
640 }
641 return "";
642}
643
645{
646 // Set first source to dummy value to force recalculation
648
649 // Keep sampling until valid contributions found (sum = 1, all >= 0)
650 while (GetContributionVector(false).min() < 0 || GetContributionVector(false).sum() > 1)
651 {
652 for (size_t i = 0; i < samplesetsorder_.size() - 1; i++)
653 {
654 SetContribution(i, unitrandom());
655 }
656 }
657
658 return true;
659}
660
662{
663 // Set first source to dummy value
665
666 // Generate random normal values
667 CVector X = getnormal(samplesetsorder_.size(), 0, 1).vec;
668
669 // Apply softmax transformation to ensure sum = 1
671
672 return true;
673}
674
675
677 const string& targetsamplename,
678 estimation_mode est_mode)
679{
680 // Populate element ordering vectors
682 selected_target_sample_ = targetsamplename;
683
684 // Validate data is loaded
685 if (empty())
686 {
687 std::cerr << "Data has not been loaded!" << std::endl;
688 return false;
689 }
690
691 numberofsourcesamplesets_ = size() - 1;
692
693 // Clear existing parameters and observations
694 parameters_.clear();
695 observations_.clear();
696 samplesetsorder_.clear();
697
698 // ========== Setup Source Contribution Parameters ==========
699
701 {
702 // Create contribution parameters for all sources except last (calculated from constraint)
703 for (auto& [group_name, profile_set] : *this)
704 {
705 if (group_name != GetTargetGroup())
706 {
707 Parameter p;
708 p.SetName(group_name + "_contribution");
710 p.SetRange(0, 1);
711 parameters_.push_back(p);
712 samplesetsorder_.push_back(group_name);
713 }
714 }
715 // Remove last contribution parameter (calculated from sum constraint)
716 parameters_.pop_back();
717 }
718 else
719 {
720 // Build source order without creating parameters
721 for (auto& [group_name, profile_set] : *this)
722 {
723 if (group_name != GetTargetGroup())
724 {
725 samplesetsorder_.push_back(group_name);
726 }
727 }
728 }
729
730 // ========== Count Elements and Isotopes ==========
731
734
735 for (const auto& [element_name, elem_info] : element_information_)
736 {
737 if (elem_info.include_in_analysis)
738 {
739 if (elem_info.Role == element_information::role::element) {
741 }
742 else if (elem_info.Role == element_information::role::isotope) {
744 }
745 }
746 }
747
748 // ========== Initialize Estimated Distributions from Fitted ==========
749
750 // For elements
751 for (const auto& [element_name, elem_info] : element_information_)
752 {
753 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
754 {
755 for (auto& [group_name, profile_set] : *this)
756 {
757 if (group_name != target_group_)
758 {
759 // Copy fitted distribution to estimated (starting point for optimization)
760 *profile_set.GetEstimatedDistribution(element_name) =
761 *profile_set.GetFittedDistribution(element_name);
762 }
763 }
764 }
765 }
766
767 // For isotopes
768 for (const auto& [element_name, elem_info] : element_information_)
769 {
770 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
771 {
772 for (auto& [group_name, profile_set] : *this)
773 {
774 if (group_name != target_group_)
775 {
776 *profile_set.GetEstimatedDistribution(element_name) =
777 *profile_set.GetFittedDistribution(element_name);
778 }
779 }
780 }
781 }
782
783 // ========== Setup Source Profile Parameters (if estimating profiles) ==========
784
786 {
787 // Mu parameters for elements (lognormal)
788 for (const auto& [element_name, elem_info] : element_information_)
789 {
790 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
791 {
792 for (auto& [group_name, profile_set] : *this)
793 {
794 if (group_name != target_group_)
795 {
796 Parameter p;
797 p.SetName(group_name + "_" + element_name + "_mu");
799
800 // Set estimated distribution type
801 profile_set.GetEstimatedDistribution(element_name)->SetType(
803 );
804
805 // Set parameter range around fitted mu
806 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
807 p.SetRange(fitted->parameters[0] - 0.2, fitted->parameters[0] + 0.2);
808
809 parameters_.push_back(p);
810 }
811 }
812 }
813 }
814
815 // Mu parameters for isotopes (normal)
816 for (const auto& [element_name, elem_info] : element_information_)
817 {
818 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
819 {
820 for (auto& [group_name, profile_set] : *this)
821 {
822 if (group_name != target_group_)
823 {
824 Parameter p;
825 p.SetName(group_name + "_" + element_name + "_mu");
827
828 // Set estimated distribution type
829 profile_set.GetEstimatedDistribution(element_name)->SetType(
831 );
832
833 // Set parameter range around fitted mu
834 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
835 p.SetRange(fitted->parameters[0] - 0.2, fitted->parameters[0] + 0.2);
836
837 parameters_.push_back(p);
838 }
839 }
840 }
841 }
842
843 // Sigma parameters for elements
844 for (const auto& [element_name, elem_info] : element_information_)
845 {
846 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
847 {
848 for (auto& [group_name, profile_set] : *this)
849 {
850 if (group_name != target_group_)
851 {
852 Parameter p;
853 p.SetName(group_name + "_" + element_name + "_sigma");
855
856 // Set parameter range around fitted sigma (with bounds)
857 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
858 double lower = std::max(fitted->parameters[1] * 0.8, 0.001);
859 double upper = std::max(fitted->parameters[1] / 0.8, 2.0);
860 p.SetRange(lower, upper);
861
862 parameters_.push_back(p);
863 }
864 }
865 }
866 }
867
868 // Sigma parameters for isotopes
869 for (const auto& [element_name, elem_info] : element_information_)
870 {
871 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
872 {
873 for (auto& [group_name, profile_set] : *this)
874 {
875 if (group_name != target_group_)
876 {
877 Parameter p;
878 p.SetName(group_name + "_" + element_name + "_sigma");
880
881 // Set parameter range around fitted sigma (with bounds)
882 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
883 double lower = std::max(fitted->parameters[1] * 0.8, 0.001);
884 double upper = std::max(fitted->parameters[1] / 0.8, 2.0);
885 p.SetRange(lower, upper);
886
887 parameters_.push_back(p);
888 }
889 }
890 }
891 }
892 }
893
894 // ========== Setup Error and Observation Parameters ==========
895
897 {
898 // Error standard deviation for elements
899 Parameter error_param;
900 error_param.SetName("Error STDev");
902 error_param.SetRange(0.01, 0.1);
903 parameters_.push_back(error_param);
904
905 // Create observations for elements
906 for (const auto& [element_name, elem_info] : element_information_)
907 {
908 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
909 {
910 Observation obs;
911 obs.SetName(targetsamplename + "_" + element_name);
912 obs.AppendValues(0, GetElementalProfile(targetsamplename)->GetValue(element_name));
913 observations_.push_back(obs);
914 }
915 }
916
917 // Error standard deviation for isotopes
918 Parameter error_param_isotope;
919 error_param_isotope.SetName("Error STDev for isotopes");
921 error_param_isotope.SetRange(0.01, 0.1);
922 parameters_.push_back(error_param_isotope);
923
924 // Create observations for isotopes
925 for (const auto& [element_name, elem_info] : element_information_)
926 {
927 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
928 {
929 Observation obs;
930 obs.SetName(targetsamplename + "_" + element_name);
931 obs.AppendValues(0, GetElementalProfile(targetsamplename)->GetValue(element_name));
932 observations_.push_back(obs);
933 }
934 }
935 }
936
937 return true;
938}
939
940
942{
943 CVector contributions(size()-1);
944 for (unsigned long int i=0; i<size()-2; i++)
945 {
946 contributions[i] = parameter(i)->Value();
947 }
948 contributions[size()-2] = 1 - contributions.sum();
949 return contributions;
950}
951
952
954{
955 if (GetSourceContributions().min()<0)
956 return -1e10;
957 else
958 return 0;
959}
960
962{
963 double logLikelihood = 0;
964 for (unsigned int element_counter=0; element_counter<element_order_.size(); element_counter++)
965 {
966 for (unsigned int source_group_counter=0; source_group_counter<numberofsourcesamplesets_; source_group_counter++)
967 {
968 Elemental_Profile_Set *this_source_group = GetSampleSet(samplesetsorder_[source_group_counter]);
969
970
971 for (map<string,Elemental_Profile>::iterator sample = this_source_group->begin(); sample!=this_source_group->end(); sample++)
972 {
973 logLikelihood += this_source_group->GetElementDistribution(element_order_[element_counter])->GetEstimatedDistribution()->EvalLog(sample->second.GetValue(element_order_[element_counter]));
974 }
975
976 }
977 }
978 return logLikelihood;
979}
980
981CVector SourceSinkData::ObservedDataforSelectedSample(const string &SelectedTargetSample)
982{
983 CVector observed_data(element_order_.size());
984 for (unsigned int i=0; i<element_order_.size(); i++)
985 { if (SelectedTargetSample!="")
986 observed_data[i] = this->GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(element_order_[i]);
987 else if (selected_target_sample_!="")
989 }
990 return observed_data;
991}
992
993CVector SourceSinkData::ObservedDataforSelectedSample_Isotope(const string &SelectedTargetSample)
994{
995 CVector observed_data(isotope_order_.size());
996 for (unsigned int i=0; i<isotope_order_.size(); i++)
997 { string corresponding_element = element_information_[isotope_order_[i]].base_element;
998 if (SelectedTargetSample!="")
999 observed_data[i] = (this->GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(isotope_order_[i])/double(1000)+1.0)*element_information_[isotope_order_[i]].standard_ratio*GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(corresponding_element);
1000 else if (selected_target_sample_!="")
1001 observed_data[i] = (this->GetSampleSet(GetTargetGroup())->GetProfile(selected_target_sample_)->GetValue(isotope_order_[i])/double(1000)+1.0)*element_information_[isotope_order_[i]].standard_ratio*GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(corresponding_element);
1002 }
1003 return observed_data;
1004}
1005
1006CVector SourceSinkData::ObservedDataforSelectedSample_Isotope_delta(const string &SelectedTargetSample)
1007{
1008 CVector observed_data(isotope_order_.size());
1009 for (unsigned int i=0; i<isotope_order_.size(); i++)
1010 { string corresponding_element = element_information_[isotope_order_[i]].base_element;
1011 if (SelectedTargetSample!="")
1012 observed_data[i] = this->GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(isotope_order_[i]);
1013 else if (selected_target_sample_!="")
1015 }
1016 return observed_data;
1017}
1018
1019
1021{
1022 // Determine parameter mode based on estimation mode
1026
1027 // Get predicted and observed concentrations
1028 CVector predicted_concentrations = PredictTarget(param_mode);
1029 CVector observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_);
1030
1031 // Check validity of predictions (all values must be positive for log-transformation)
1032 if (predicted_concentrations.min() <= 0)
1033 {
1034 return -1e10; // Invalid prediction: return extremely low likelihood
1035 }
1036
1037 // Calculate log-likelihood in log-space
1038 // Formula: log(L) = -n*log(σ) - ||log(C_pred) - log(C_obs)||² / (2σ²)
1039 const double num_elements = predicted_concentrations.num;
1040 const double normalization_term = num_elements * log(error_stdev_);
1041
1042 CVector log_residuals = predicted_concentrations.Log() - observed_concentrations.Log();
1043 const double sum_squared_residuals = pow(log_residuals.norm2(), 2);
1044 const double variance_term = 2.0 * pow(error_stdev_, 2);
1045
1046 double log_likelihood = -normalization_term - (sum_squared_residuals / variance_term);
1047
1048 return log_likelihood;
1049}
1050
1052{
1053 // Determine parameter mode based on estimation mode
1057
1058 // Get predicted and observed isotopic delta values
1059 CVector predicted_deltas = PredictTarget_Isotope_delta(param_mode);
1061
1062 // Calculate log-likelihood in linear space (delta values)
1063 // Formula: log(L) = -n*log(σ_iso) - ||δ_pred - δ_obs||² / (2σ_iso²)
1064 const double num_isotopes = predicted_deltas.num;
1065 const double normalization_term = num_isotopes * log(error_stdev_isotope_);
1066
1067 CVector residuals = predicted_deltas - observed_deltas;
1068 const double sum_squared_residuals = pow(residuals.norm2(), 2);
1069 const double variance_term = 2.0 * pow(error_stdev_isotope_, 2);
1070
1071 double log_likelihood = -normalization_term - (sum_squared_residuals / variance_term);
1072
1073 return log_likelihood;
1074}
1075
1076
1078{
1079 // Get predicted values using direct parameter mode
1080 CVector predicted_concentrations = PredictTarget(parameter_mode::direct);
1081 CVector predicted_deltas = PredictTarget_Isotope_delta(parameter_mode::direct);
1082
1083 // Check for invalid predictions
1084 if (!predicted_concentrations.is_finite())
1085 {
1086 qDebug() << "Warning: Non-finite predicted concentrations detected in ResidualVector()";
1087 }
1088
1089 // Get observed values for the selected target sample
1090 CVector observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_);
1092
1093 // Calculate residuals
1094 // Elemental: log-space residuals (log(predicted) - log(observed))
1095 CVector elemental_residuals = predicted_concentrations.Log() - observed_concentrations.Log();
1096
1097 // Isotopic: linear residuals (predicted_delta - observed_delta)
1098 CVector isotopic_residuals = predicted_deltas - observed_deltas;
1099
1100 // Combine residuals: [elemental_residuals, isotopic_residuals]
1101 CVector combined_residuals = elemental_residuals;
1102 combined_residuals.append(isotopic_residuals);
1103
1104 // Check for non-finite residuals (indicates numerical issues)
1105 //if (!combined_residuals.is_finite())
1106 //{
1107 // qDebug() << "Warning: Non-finite residuals detected in ResidualVector()";
1108 // qDebug() << "Contribution vector:" << GetContributionVector().toString();
1109 // qDebug() << "Contribution vector (softmax):" << GetContributionVectorSoftmax().toString();
1110 //}
1111
1112 return combined_residuals;
1113}
1114
1116{
1117 // Get predicted values (returns CVector with .vec member for Armadillo compatibility)
1118 CVector_arma predicted_concentrations = PredictTarget().vec;
1119 CVector_arma predicted_deltas = PredictTarget_Isotope_delta().vec;
1120
1121 // Get observed values for the selected target sample
1122 CVector_arma observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_).vec;
1123 CVector_arma observed_deltas = ObservedDataforSelectedSample_Isotope_delta(selected_target_sample_).vec;
1124
1125 // Calculate residuals
1126 // Elemental: log-space residuals (log(predicted) - log(observed))
1127 CVector_arma elemental_residuals = predicted_concentrations.Log() - observed_concentrations.Log();
1128
1129 // Isotopic: linear residuals (predicted_delta - observed_delta)
1130 CVector_arma isotopic_residuals = predicted_deltas - observed_deltas;
1131
1132 // Combine residuals: [elemental_residuals, isotopic_residuals]
1133 CVector_arma combined_residuals = elemental_residuals;
1134 combined_residuals.append(isotopic_residuals);
1135
1136 return combined_residuals;
1137}
1138
1139
1141{
1142 const size_t num_sources = GetSourceOrder().size();
1143 const size_t num_parameters = num_sources - 1; // Last contribution is implicit
1144 const size_t num_residuals = element_order_.size() + isotope_order_.size();
1145
1146 CMatrix_arma jacobian(num_parameters, num_residuals);
1147
1148 // Store base state
1149 CVector_arma base_contributions = GetContributionVector(false).vec;
1150 CVector_arma base_residuals = ResidualVector_arma();
1151
1152 // Compute derivatives using finite differences
1153 for (unsigned int i = 0; i < num_parameters; i++)
1154 {
1155 // Adaptive epsilon: smaller when far from 0.5, larger when near boundaries
1156 const double epsilon = (0.5 - base_contributions[i]) * 1e-6;
1157
1158 // Perturb contribution
1159 SetContribution(i, base_contributions[i] + epsilon);
1160 CVector_arma perturbed_residuals = ResidualVector_arma();
1161
1162 // Compute derivative: ∂residuals/∂contribution_i
1163 jacobian.setcol(i, (perturbed_residuals - base_residuals) / epsilon);
1164
1165 // Restore original contribution
1166 SetContribution(i, base_contributions[i]);
1167 }
1168
1169 return jacobian;
1170}
1171
1173{
1174 const size_t num_sources = GetSourceOrder().size();
1175 const size_t num_parameters = num_sources - 1; // Last contribution is implicit
1176 const size_t num_residuals = element_order_.size() + isotope_order_.size();
1177
1178 CMatrix jacobian(num_parameters, num_residuals);
1179
1180 // Store base state
1181 CVector base_contributions = GetContributionVector(false);
1182 CVector base_residuals = ResidualVector();
1183
1184 // Compute derivatives using finite differences
1185 for (unsigned int i = 0; i < num_parameters; i++)
1186 {
1187 // Adaptive epsilon: smaller when far from 0.5, larger when near boundaries
1188 const double epsilon = (0.5 - base_contributions[i]) * 1e-3;
1189
1190 // Perturb contribution
1191 SetContribution(i, base_contributions[i] + epsilon);
1192 CVector perturbed_residuals = ResidualVector();
1193
1194 // Compute derivative: ∂residuals/∂contribution_i
1195 jacobian.setrow(i, (perturbed_residuals - base_residuals) / epsilon);
1196
1197 // Restore original contribution
1198 SetContribution(i, base_contributions[i]);
1199 }
1200
1201 return jacobian;
1202}
1203
1205{
1206 const size_t num_sources = GetSourceOrder().size();
1207 const size_t num_residuals = element_order_.size() + isotope_order_.size();
1208
1209 CMatrix jacobian(num_sources, num_residuals); // All sources are parameters in softmax
1210
1211 // Store base state
1212 CVector base_softmax_params = GetContributionVectorSoftmax();
1213 CVector base_residuals = ResidualVector();
1214
1215 // Compute derivatives using finite differences
1216 for (unsigned int i = 0; i < num_sources; i++)
1217 {
1218 // Sign-dependent epsilon for better numerical behavior
1219 const double epsilon = -sign(base_softmax_params[i]) * 1e-3;
1220
1221 // Perturb softmax parameter
1222 CVector perturbed_params = base_softmax_params;
1223 perturbed_params[i] += epsilon;
1224 SetContributionSoftmax(perturbed_params);
1225
1226 CVector perturbed_residuals = ResidualVector();
1227
1228 // Compute derivative: ∂residuals/∂softmax_param_i
1229 jacobian.setrow(i, (perturbed_residuals - base_residuals) / epsilon);
1230
1231 // Restore original softmax parameters
1232 SetContributionSoftmax(base_softmax_params);
1233 }
1234
1235 return jacobian;
1236}
1237
1239{
1240 // Get current residuals and Jacobian
1241 CVector residuals = ResidualVector();
1242 CMatrix jacobian = ResidualJacobian();
1243
1244 // Compute J^T J (normal equations matrix)
1245 CMatrix jacobian_transpose_jacobian = jacobian * Transpose(jacobian);
1246
1247 // Apply Levenberg-Marquardt damping: (1 + λ) on diagonal
1248 jacobian_transpose_jacobian.ScaleDiagonal(1.0 + lambda);
1249
1250 // Compute right-hand side: J^T r
1251 CVector jacobian_times_residuals = jacobian * residuals;
1252
1253 // Check for near-singular matrix and add regularization if needed
1254 if (det(jacobian_transpose_jacobian) <= 1e-6)
1255 {
1256 // Add additional regularization: λI
1257 const size_t matrix_size = jacobian_transpose_jacobian.getnumcols();
1258 CMatrix identity = CMatrix::Diag(matrix_size);
1259 jacobian_transpose_jacobian += lambda * identity;
1260 }
1261
1262 // Solve for parameter update: dx = (J^T J)^(-1) J^T r
1263 CVector parameter_update = jacobian_times_residuals / jacobian_transpose_jacobian;
1264
1265 return parameter_update;
1266}
1267
1269{
1270 // Get current residuals and Jacobian (softmax parameterization)
1271 CVector residuals = ResidualVector();
1272 CMatrix jacobian = ResidualJacobian_softmax();
1273
1274 // Compute J^T J (normal equations matrix)
1275 CMatrix jacobian_transpose_jacobian = jacobian * Transpose(jacobian);
1276
1277 // Apply Levenberg-Marquardt damping: (1 + λ) on diagonal
1278 jacobian_transpose_jacobian.ScaleDiagonal(1.0 + lambda);
1279
1280 // Compute right-hand side: J^T r
1281 CVector jacobian_times_residuals = jacobian * residuals;
1282
1283 // Check for near-singular matrix and add regularization if needed
1284 if (det(jacobian_transpose_jacobian) <= 1e-6)
1285 {
1286 // Add additional regularization: λI
1287 const size_t matrix_size = jacobian_transpose_jacobian.getnumcols();
1288 CMatrix identity = CMatrix::Diag(matrix_size);
1289 jacobian_transpose_jacobian += lambda * identity;
1290 }
1291
1292 // Solve for parameter update: dx = (J^T J)^(-1) J^T r
1293 CVector parameter_update = jacobian_times_residuals / jacobian_transpose_jacobian;
1294
1295 return parameter_update;
1296}
1297
1299{
1300 // Initialize contributions based on parameterization
1301 if (trans == transformation::linear)
1303 else if (trans == transformation::softmax)
1305
1306 // Algorithm parameters
1307 const double tolerance = 1e-10;
1308 const int max_iterations = 1000;
1309 const double improvement_threshold = 0.8; // Error must reduce to <80% for lambda decrease
1310 const double lambda_decrease_factor = 1.2;
1311 const double lambda_increase_factor = 1.2;
1312 const double lambda_no_update_factor = 5.0;
1313
1314 // Initialize tracking variables
1315 double lambda = 1.0;
1316 double current_error = ResidualVector().norm2();
1317 double previous_error = 1000.0;
1318 double initial_param_change = 10000.0;
1319 double current_param_change = 10000.0;
1320 int iteration = 0;
1321
1322 // Main optimization loop
1323 while (current_error > tolerance &&
1324 current_param_change > tolerance &&
1325 iteration < max_iterations)
1326 {
1327 // Store current parameter values
1328 CVector current_params;
1329 if (trans == transformation::linear)
1330 current_params = GetContributionVector(false);
1331 else if (trans == transformation::softmax)
1332 current_params = GetContributionVectorSoftmax();
1333
1334 previous_error = current_error;
1335
1336 // Compute parameter update step
1337 CVector parameter_update;
1338 if (trans == transformation::linear)
1339 parameter_update = OneStepLevenberg_Marquardt(lambda);
1340 else if (trans == transformation::softmax)
1341 parameter_update = OneStepLevenberg_Marquardt_softmax(lambda);
1342
1343 // Handle singular Jacobian (no valid update computed)
1344 if (parameter_update.num == 0)
1345 {
1346 lambda *= lambda_no_update_factor;
1347 continue; // Skip to next iteration
1348 }
1349
1350 // Track parameter change magnitude
1351 current_param_change = parameter_update.norm2();
1352 if (iteration == 0)
1353 initial_param_change = current_param_change;
1354
1355 // Apply parameter update
1356 CVector updated_params = current_params - parameter_update;
1357 if (trans == transformation::linear)
1358 SetContribution(updated_params);
1359 else if (trans == transformation::softmax)
1360 SetContributionSoftmax(updated_params);
1361
1362 // Evaluate new error
1363 CVector residuals = ResidualVector();
1364 current_error = residuals.norm2();
1365
1366 // Adaptive lambda adjustment based on error change
1367 if (current_error < previous_error * improvement_threshold)
1368 {
1369 // Good progress: decrease lambda (move toward Gauss-Newton)
1370 lambda /= lambda_decrease_factor;
1371 }
1372 else if (current_error > previous_error)
1373 {
1374 // Error increased: reject step, increase lambda (move toward gradient descent)
1375 lambda *= lambda_increase_factor;
1376
1377 // Restore previous parameters
1378 if (trans == transformation::linear)
1379 SetContribution(current_params);
1380 else if (trans == transformation::softmax)
1381 SetContributionSoftmax(current_params);
1382
1383 current_error = previous_error;
1384 }
1385 // else: modest improvement, keep lambda unchanged
1386
1387 // Update progress visualization if available
1388 if (rtw_)
1389 {
1390 rtw_->AppendPoint(iteration, current_error);
1391 rtw_->SetXRange(0, iteration);
1392 rtw_->SetProgress(1.0 - current_param_change / initial_param_change);
1393 }
1394
1395 iteration++;
1396 }
1397
1398 // TODO: Return convergence status instead of always false
1399 return false;
1400}
1401
1402
1404{
1405 // Compute predicted concentrations: C = Source_Matrix × Contribution_Vector
1406 CMatrix source_mean_matrix = BuildSourceMeanMatrix(param_mode);
1407 CVector contribution_vector = GetContributionVector();
1408 CVector predicted_concentrations = source_mean_matrix * contribution_vector;
1409
1410 // Update stored predicted values in observation objects for each element
1411 for (unsigned int i = 0; i < element_order_.size(); i++)
1412 {
1413 observation(i)->SetPredictedValue(predicted_concentrations[i]);
1414 }
1415
1416 return predicted_concentrations;
1417}
1418
1420{
1421 // Compute predicted isotopic concentrations: C = Source_Matrix × Contribution_Vector
1422 CMatrix source_isotope_matrix = BuildSourceMeanMatrix_Isotopes(param_mode);
1423 CVector contribution_vector = GetContributionVector();
1424 CVector predicted_isotope_concentrations = source_isotope_matrix * contribution_vector;
1425
1426 return predicted_isotope_concentrations;
1427}
1428
1430{
1431 CMatrix SourceMeanMat = BuildSourceMeanMatrix(param_mode);
1432 CMatrix SourceMeanMat_Iso = BuildSourceMeanMatrix_Isotopes(param_mode);
1433 CVector C_elements = SourceMeanMat*GetContributionVector();
1434 CVector C = SourceMeanMat_Iso*GetContributionVector();
1435 for (unsigned int i=0; i<numberofisotopes_; i++)
1436 {
1437 string corresponding_element = element_information_[isotope_order_[i]].base_element;
1438 double predicted_corresponding_element_concentration = C_elements[lookup(element_order_,corresponding_element)];
1439 double ratio = C[i]/predicted_corresponding_element_concentration;
1440 double standard_ratio = element_information_[isotope_order_[i]].standard_ratio;
1441 C[i] = (ratio/standard_ratio-1.0)*1000.0;
1442 }
1443 for (unsigned int i=element_order_.size(); i<element_order_.size()+isotope_order_.size(); i++)
1444 {
1446 }
1447 return C;
1448}
1449
1450
1455
1457{
1458 // Initialize likelihood components
1459 double source_data_log_likelihood = 0.0;
1460 double element_observation_log_likelihood = 0.0;
1461 double isotope_observation_log_likelihood = 0.0;
1462
1463 // Component 1: Log-likelihood of source data given estimated distributions
1464 // P(Y | μ, σ) - Only included when estimating source profiles
1465 if (est_mode != estimation_mode::only_contributions) {
1466 source_data_log_likelihood = LogLikelihoodSourceElementalDistributions();
1467 }
1468
1469 // Component 2: Log-prior on contributions
1470 // P(f) - Always included (uniform Dirichlet prior)
1471 double contribution_log_prior = LogPriorContributions();
1472
1473 // Component 3: Log-likelihood of observations given model predictions
1474 // P(C_obs | f, μ, σ) - Only when fitting to target sample
1476 // Elements: log P(C_obs | C_pred)
1477 element_observation_log_likelihood = LogLikelihoodModelvsMeasured(est_mode);
1478
1479 // Isotopes: log P(δ_obs | δ_pred)
1480 isotope_observation_log_likelihood = LogLikelihoodModelvsMeasured_Isotope(est_mode);
1481 }
1482
1483 // Sum all components to get total log-likelihood
1484 double total_log_likelihood = source_data_log_likelihood +
1485 element_observation_log_likelihood +
1486 isotope_observation_log_likelihood +
1487 contribution_log_prior;
1488
1489 return total_log_likelihood;
1490}
1491
1493{
1494 const size_t num_elements = element_order_.size();
1495 const size_t num_sources = numberofsourcesamplesets_;
1496
1497 // Initialize source mean matrix: rows = elements, cols = sources
1498 // Matrix entry [i,j] = mean concentration of element i in source j
1499 CMatrix source_means(num_elements, num_sources);
1500
1501 // Fill matrix with mean concentrations for each element and source
1502 for (size_t element_idx = 0; element_idx < num_elements; element_idx++)
1503 {
1504 const string& element_name = element_order_[element_idx];
1505
1506 for (size_t source_idx = 0; source_idx < num_sources; source_idx++)
1507 {
1508 const string& source_name = samplesetsorder_[source_idx];
1509 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1510
1511 // Get estimated distribution for this element in this source
1512 Distribution* element_dist = source_group->GetEstimatedDistribution(element_name);
1513
1514 // Calculate mean based on parameter mode
1516 // Parametric mean from distribution parameters (μ, σ)
1517 // For lognormal: Mean = exp(μ + σ²/2)
1518 source_means[element_idx][source_idx] = element_dist->Mean();
1519 }
1520 else {
1521 // Empirical mean calculated directly from data points
1522 source_means[element_idx][source_idx] = element_dist->DataMean();
1523 }
1524 }
1525 }
1526
1527 return source_means;
1528}
1529
1531{
1532 const size_t num_isotopes = isotope_order_.size();
1533 const size_t num_sources = numberofsourcesamplesets_;
1534
1535 // Initialize source isotope matrix: rows = isotopes, cols = sources
1536 // Matrix entry [i,j] = mean absolute concentration of isotope i in source j
1537 CMatrix source_isotope_means(num_isotopes, num_sources);
1538
1539 // Fill matrix with mean isotope concentrations (converted from delta)
1540 for (size_t isotope_idx = 0; isotope_idx < num_isotopes; isotope_idx++)
1541 {
1542 const string& isotope_name = isotope_order_[isotope_idx];
1543 const element_information& isotope_info = element_information_[isotope_name];
1544 const string& base_element = isotope_info.base_element;
1545 const double standard_ratio = isotope_info.standard_ratio;
1546
1547 for (size_t source_idx = 0; source_idx < num_sources; source_idx++)
1548 {
1549 const string& source_name = samplesetsorder_[source_idx];
1550 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1551
1552 // Get distributions for isotope and its base element
1553 Distribution* isotope_dist = source_group->GetEstimatedDistribution(isotope_name);
1554 Distribution* base_element_dist = source_group->GetEstimatedDistribution(base_element);
1555
1556 // Retrieve mean values based on parameter mode
1557 double mean_delta;
1558 double mean_base_concentration;
1559
1561 // Use parametric means from fitted distributions
1562 mean_delta = isotope_dist->Mean();
1563 mean_base_concentration = base_element_dist->Mean();
1564 }
1565 else {
1566 // Use empirical means from actual data
1567 mean_delta = isotope_dist->DataMean();
1568 mean_base_concentration = base_element_dist->DataMean();
1569 }
1570
1571 // Convert delta notation to absolute concentration
1572 // Formula: [isotope] = (δ/1000 + 1) × R_standard × [base_element]
1573 double delta_ratio = (mean_delta / 1000.0) + 1.0;
1574 double absolute_isotope_concentration = delta_ratio * standard_ratio * mean_base_concentration;
1575
1576 source_isotope_means[isotope_idx][source_idx] = absolute_isotope_concentration;
1577 }
1578 }
1579
1580 return source_isotope_means;
1581}
1582
1584{
1585 // Determine vector size based on whether to include constrained contribution
1586 const size_t num_contributions = include_all ?
1589
1590 CVector contributions(num_contributions);
1591
1592 // Retrieve contribution values from each source group
1593 for (size_t source_idx = 0; source_idx < num_contributions; source_idx++)
1594 {
1595 const string& source_name = samplesetsorder_[source_idx];
1596 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1597 contributions[source_idx] = source_group->GetContribution();
1598 }
1599
1600 return contributions;
1601}
1602
1604{
1605 const size_t num_sources = numberofsourcesamplesets_;
1606 CVector softmax_parameters(num_sources);
1607
1608 // Retrieve softmax parameter values from each source group
1609 for (size_t source_idx = 0; source_idx < num_sources; source_idx++)
1610 {
1611 const string& source_name = samplesetsorder_[source_idx];
1612 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1613 softmax_parameters[source_idx] = source_group->GetContributionSoftmax();
1614 }
1615
1616 return softmax_parameters;
1617}
1618
1619
1620
1621
1622void SourceSinkData::SetContribution(size_t source_index, double contribution_value)
1623{
1624 // Set the specified contribution
1625 const string& source_name = samplesetsorder_[source_index];
1626 GetSampleSet(source_name)->SetContribution(contribution_value);
1627
1628 // Update last contribution to maintain sum constraint
1629 const size_t last_source_index = samplesetsorder_.size() - 1;
1630 const string& last_source_name = samplesetsorder_[last_source_index];
1631
1632 // Calculate constrained contribution: c_last = 1 - Σ(c_i)
1633 double sum_of_independent = GetContributionVector(false).sum();
1634 double constrained_contribution = 1.0 - sum_of_independent;
1635
1636 GetSampleSet(last_source_name)->SetContribution(constrained_contribution);
1637}
1638
1639void SourceSinkData::SetContributionSoftmax(size_t source_index, double softmax_value)
1640{
1641 // Set softmax parameter (no constraint to maintain)
1642 const string& source_name = samplesetsorder_[source_index];
1643 GetSampleSet(source_name)->SetContributionSoftmax(softmax_value);
1644}
1645
1646void SourceSinkData::SetContribution(const CVector& contributions)
1647{
1648 // Set each contribution (last contribution updated automatically each time)
1649 for (size_t i = 0; i < static_cast<size_t>(contributions.num); i++)
1650 {
1651 SetContribution(i, contributions.vec[i]);
1652 }
1653
1654 // Validate: all contributions should be non-negative
1655 if (contributions.min() < 0.0) {
1656 std::cerr << "Warning: Negative contribution detected in SetContribution()" << std::endl;
1657 std::cerr << " Min value: " << contributions.min() << std::endl;
1658 }
1659}
1660
1661void SourceSinkData::SetContributionSoftmax(const CVector& softmax_params)
1662{
1663 // Compute softmax normalization denominator: Σ exp(x_i)
1664 double denominator = 0.0;
1665 for (size_t i = 0; i < static_cast<size_t>(softmax_params.num); i++)
1666 {
1667 denominator += exp(softmax_params[i]);
1668 }
1669
1670 // Apply softmax transformation: c_i = exp(x_i) / Σ exp(x_j)
1671 for (size_t i = 0; i < static_cast<size_t>(softmax_params.num); i++)
1672 {
1673 double softmax_param = softmax_params.at(i);
1674 double contribution = exp(softmax_param) / denominator;
1675
1676 // Set both softmax parameter and computed contribution
1677 SetContributionSoftmax(i, softmax_param);
1679 }
1680
1681 // Validate: resulting contributions should be valid
1682 CVector resulting_contributions = GetContributionVector();
1683 if (resulting_contributions.min() < 0.0) {
1684 std::cerr << "Warning: Invalid contributions after softmax transformation" << std::endl;
1685 std::cerr << " Min contribution: " << resulting_contributions.min() << std::endl;
1686 std::cerr << " This should not happen with softmax - check for numerical issues" << std::endl;
1687 }
1688}
1689
1691 size_t element_index,
1692 size_t source_index)
1693{
1694 // Parameter vector layout:
1695 // [0 to n-2]: Contribution parameters (n-1 independent contributions)
1696 // [n-1 onwards]: Element μ parameters
1697
1698 const size_t num_contribution_params = size() - 1; // Exclude target group
1699 const size_t element_mu_base_index = num_contribution_params;
1700
1701 // Calculate index for this element's μ in this source
1702 // Elements are stored in blocks: all sources for element 0, then all sources for element 1, etc.
1703 const size_t parameter_index = element_mu_base_index +
1704 (element_index * numberofsourcesamplesets_) +
1705 source_index;
1706
1707 // Bounds check
1708 if (parameter_index >= parameters_.size()) {
1709 std::cerr << "Error: Parameter index out of bounds in GetElementDistributionMuParameter" << std::endl;
1710 return nullptr;
1711 }
1712
1713 return &parameters_[parameter_index];
1714}
1715
1717 size_t element_index,
1718 size_t source_index)
1719{
1720 // Parameter vector layout:
1721 // [0 to n-2]: Contribution parameters
1722 // [n-1 to n-1 + num_elements × num_sources - 1]: Element μ parameters
1723 // [next block]: Element σ parameters ← We're here
1724
1725 const size_t num_contribution_params = size() - 1;
1726 const size_t num_element_mu_params = numberofconstituents_ * numberofsourcesamplesets_;
1727 const size_t element_sigma_base_index = num_contribution_params + num_element_mu_params;
1728
1729 // Calculate index for this element's σ in this source
1730 const size_t parameter_index = element_sigma_base_index +
1731 (element_index * numberofsourcesamplesets_) +
1732 source_index;
1733
1734 // Bounds check
1735 if (parameter_index >= parameters_.size()) {
1736 std::cerr << "Error: Parameter index out of bounds in GetElementDistributionSigmaParameter" << std::endl;
1737 return nullptr;
1738 }
1739
1740 return &parameters_[parameter_index];
1741}
1742
1744 size_t element_index,
1745 size_t source_index)
1746{
1747 Parameter* mu_param = GetElementDistributionMuParameter(element_index, source_index);
1748
1749 if (mu_param == nullptr) {
1750 std::cerr << "Warning: Unable to retrieve μ parameter for element "
1751 << element_index << ", source " << source_index << std::endl;
1752 return 0.0;
1753 }
1754
1755 return mu_param->Value();
1756}
1757
1759 size_t element_index,
1760 size_t source_index)
1761{
1762 Parameter* sigma_param = GetElementDistributionSigmaParameter(element_index, source_index);
1763
1764 if (sigma_param == nullptr) {
1765 std::cerr << "Warning: Unable to retrieve σ parameter for element "
1766 << element_index << ", source " << source_index << std::endl;
1767 return 0.0;
1768 }
1769
1770 return sigma_param->Value();
1771}
1772
1773bool SourceSinkData::SetParameterValue(size_t index, double value)
1774{
1775 // Validate index
1776 if (index >= parameters_.size()) {
1777 return false;
1778 }
1779
1780 // Determine which parameter blocks are active based on estimation mode
1781 const bool estimating_contributions =
1783 const bool estimating_profiles =
1785
1786 // Calculate parameter block boundaries
1787 const size_t num_contribution_params = estimating_contributions ? (numberofsourcesamplesets_ - 1) : 0;
1788 const size_t num_element_mu_params = numberofconstituents_ * numberofsourcesamplesets_;
1789 const size_t num_isotope_mu_params = numberofisotopes_ * numberofsourcesamplesets_;
1790 const size_t num_element_sigma_params = num_element_mu_params;
1791 const size_t num_isotope_sigma_params = num_isotope_mu_params;
1792
1793 // Update parameter value
1794 parameters_[index].SetValue(value);
1795
1796 // ========== Block 1: Contribution Parameters ==========
1797 if (estimating_contributions && index < num_contribution_params)
1798 {
1799 // Update this source's contribution
1801
1802 // Update last contribution to maintain sum constraint: c_n = 1 - Σ(c_1...c_{n-1})
1803 const size_t last_source_index = numberofsourcesamplesets_ - 1;
1804 double sum_of_independent = GetContributionVector(false).sum();
1805 GetSampleSet(samplesetsorder_[last_source_index])->SetContribution(1.0 - sum_of_independent);
1806
1807 return true;
1808 }
1809
1810 // ========== Block 2: Element μ Parameters ==========
1811 size_t element_mu_start = num_contribution_params;
1812 size_t element_mu_end = element_mu_start + num_element_mu_params;
1813
1814 if (estimating_profiles &&
1816 index >= element_mu_start && index < element_mu_end)
1817 {
1818 size_t offset = index - element_mu_start;
1819 size_t element_index = offset / numberofsourcesamplesets_;
1820 size_t source_index = offset % numberofsourcesamplesets_;
1821
1822 GetElementDistribution(element_order_[element_index], samplesetsorder_[source_index])
1823 ->SetEstimatedMu(value);
1824
1825 return true;
1826 }
1827
1828 // ========== Block 3: Isotope μ Parameters ==========
1829 size_t isotope_mu_start = element_mu_end;
1830 size_t isotope_mu_end = isotope_mu_start + num_isotope_mu_params;
1831
1832 if (estimating_profiles &&
1834 index >= isotope_mu_start && index < isotope_mu_end)
1835 {
1836 size_t offset = index - isotope_mu_start;
1837 size_t isotope_index = offset / numberofsourcesamplesets_;
1838 size_t source_index = offset % numberofsourcesamplesets_;
1839
1840 GetElementDistribution(isotope_order_[isotope_index], samplesetsorder_[source_index])
1841 ->SetEstimatedMu(value);
1842
1843 return true;
1844 }
1845
1846 // ========== Block 4: Element σ Parameters ==========
1847 size_t element_sigma_start = isotope_mu_end;
1848 size_t element_sigma_end = element_sigma_start + num_element_sigma_params;
1849
1850 if (estimating_profiles &&
1852 index >= element_sigma_start && index < element_sigma_end)
1853 {
1854 if (value < 0.0) {
1855 std::cerr << "Error: Element σ parameter cannot be negative (value = "
1856 << value << ")" << std::endl;
1857 return false;
1858 }
1859
1860 size_t offset = index - element_sigma_start;
1861 size_t element_index = offset / numberofsourcesamplesets_;
1862 size_t source_index = offset % numberofsourcesamplesets_;
1863
1864 GetElementDistribution(element_order_[element_index], samplesetsorder_[source_index])
1865 ->SetEstimatedSigma(value);
1866
1867 return true;
1868 }
1869
1870 // ========== Block 5: Isotope σ Parameters ==========
1871 size_t isotope_sigma_start = element_sigma_end;
1872 size_t isotope_sigma_end = isotope_sigma_start + num_isotope_sigma_params;
1873
1874 if (estimating_profiles &&
1876 index >= isotope_sigma_start && index < isotope_sigma_end)
1877 {
1878 if (value < 0.0) {
1879 std::cerr << "Error: Isotope σ parameter cannot be negative (value = "
1880 << value << ")" << std::endl;
1881 return false;
1882 }
1883
1884 size_t offset = index - isotope_sigma_start;
1885 size_t isotope_index = offset / numberofsourcesamplesets_;
1886 size_t source_index = offset % numberofsourcesamplesets_;
1887
1888 GetElementDistribution(isotope_order_[isotope_index], samplesetsorder_[source_index])
1889 ->SetEstimatedSigma(value);
1890
1891 return true;
1892 }
1893
1894 // ========== Block 6: Element Error Standard Deviation ==========
1895 size_t error_element_index = isotope_sigma_end;
1896
1897 if (estimating_contributions && index == error_element_index)
1898 {
1899 if (value < 0.0) {
1900 std::cerr << "Error: Element error std dev cannot be negative (value = "
1901 << value << ")" << std::endl;
1902 return false;
1903 }
1904
1905 error_stdev_ = value;
1906 return true;
1907 }
1908
1909 // ========== Block 7: Isotope Error Standard Deviation ==========
1910 size_t error_isotope_index = error_element_index + 1;
1911
1912 if (estimating_contributions && index == error_isotope_index)
1913 {
1914 if (value < 0.0) {
1915 std::cerr << "Error: Isotope error std dev cannot be negative (value = "
1916 << value << ")" << std::endl;
1917 return false;
1918 }
1919
1920 error_stdev_isotope_ = value;
1921 return true;
1922 }
1923
1924 // If we reach here, index doesn't match any known parameter block
1925 std::cerr << "Warning: Parameter index " << index << " not recognized" << std::endl;
1926 return false;
1927}
1928
1929double SourceSinkData::GetParameterValue(size_t index) const
1930{
1931 return parameters_[index].Value();
1932}
1933
1934bool SourceSinkData::SetParameterValue(const CVector& values)
1935{
1936 bool all_successful = true;
1937
1938 // Update each parameter sequentially
1939 for (size_t i = 0; i < static_cast<size_t>(values.num); i++)
1940 {
1941 bool success = SetParameterValue(i, values[i]);
1942 all_successful &= success;
1943
1944 // Note: Continue even if one fails to attempt all updates
1945 }
1946
1947 return all_successful;
1948}
1949
1951{
1952 CVector parameter_values(parameters_.size());
1953
1954 // Extract value from each parameter
1955 for (size_t i = 0; i < parameters_.size(); i++)
1956 {
1957 parameter_values[i] = parameters_[i].Value();
1958 }
1959
1960 return parameter_values;
1961}
1962
1963CVector SourceSinkData::Gradient(const CVector& parameters, estimation_mode est_mode)
1964{
1965 CVector gradient(parameters.num);
1966 CVector perturbed_params = parameters;
1967
1968 // Set base parameters and evaluate baseline log-likelihood
1969 SetParameterValue(parameters);
1970 double baseline_log_likelihood = LogLikelihood(est_mode);
1971
1972 // Compute partial derivative for each parameter using finite differences
1973 for (size_t i = 0; i < static_cast<size_t>(parameters.num); i++)
1974 {
1975 // Perturb parameter i by epsilon
1976 perturbed_params[i] += epsilon_;
1977 SetParameterValue(perturbed_params);
1978
1979 // Evaluate log-likelihood at perturbed point
1980 double perturbed_log_likelihood = LogLikelihood(est_mode);
1981
1982 // Compute numerical derivative: ∂log(L)/∂θ_i ≈ Δlog(L) / Δθ_i
1983 gradient[i] = (perturbed_log_likelihood - baseline_log_likelihood) / epsilon_;
1984
1985 // Restore parameter for next iteration
1986 perturbed_params[i] = parameters[i];
1987 }
1988
1989 // Normalize gradient to unit length for numerical stability
1990 return gradient / gradient.norm2();
1991}
1992
1994{
1995 // Get current parameters and evaluate baseline likelihood
1996 CVector current_params = GetParameterValue();
1997 double baseline_likelihood = LogLikelihood(est_mode);
1998
1999 // Compute normalized gradient direction
2000 CVector gradient_direction = Gradient(current_params, est_mode);
2001
2002 // Try two candidate steps: standard and aggressive (2×)
2003 CVector candidate_step1 = current_params + distance_coeff_ * gradient_direction;
2004 SetParameterValue(candidate_step1);
2005 double likelihood_step1 = LogLikelihood(est_mode);
2006
2007 CVector candidate_step2 = current_params + 2.0 * distance_coeff_ * gradient_direction;
2008 SetParameterValue(candidate_step2);
2009 double likelihood_step2 = LogLikelihood(est_mode);
2010
2011 // Debug output
2012 std::cout << "Distance Coefficient: " << distance_coeff_ << std::endl;
2013
2014 // Reset step size if it becomes too small
2015 if (distance_coeff_ < 1e-6) {
2016 distance_coeff_ = 1.0;
2017 }
2018
2019 // ========== Strategy 1: Aggressive step (2×) is best ==========
2020 if (likelihood_step2 > likelihood_step1 && likelihood_step2 > baseline_likelihood)
2021 {
2022 // Success with larger step - increase step size for next iteration
2023 distance_coeff_ *= 2.0;
2024 return candidate_step2;
2025 }
2026
2027 // ========== Strategy 2: Standard step (1×) is best ==========
2028 else if (likelihood_step1 > likelihood_step2 && likelihood_step1 > baseline_likelihood)
2029 {
2030 // Success with standard step - keep current step size
2031 SetParameterValue(candidate_step1);
2032 return candidate_step1;
2033 }
2034
2035 // ========== Strategy 3: Neither improved - backtrack ==========
2036 else
2037 {
2038 const int max_backtrack_iterations = 5;
2039 int backtrack_count = 0;
2040
2041 // Progressively reduce step size until we find improvement
2042 while (baseline_likelihood >= likelihood_step1 && backtrack_count < max_backtrack_iterations)
2043 {
2044 distance_coeff_ *= 0.5; // Halve the step size
2045
2046 candidate_step1 = current_params + distance_coeff_ * gradient_direction;
2047 SetParameterValue(candidate_step1);
2048 likelihood_step1 = LogLikelihood(est_mode);
2049
2050 backtrack_count++;
2051 }
2052
2053 if (backtrack_count < max_backtrack_iterations)
2054 {
2055 // Found an improvement with smaller step
2056 SetParameterValue(candidate_step1);
2057 return candidate_step1;
2058 }
2059 else
2060 {
2061 // No improvement found after backtracking - return to original
2062 std::cerr << "Warning: No improvement found after " << max_backtrack_iterations
2063 << " backtracking iterations" << std::endl;
2064 SetParameterValue(current_params);
2065 return current_params;
2066 }
2067 }
2068}
2069
2071{
2072 // Reset counters and ordering
2074 constituent_order_.clear();
2075
2076 // Collect all chemical elements (exclude isotopes, size, organic carbon)
2077 for (const auto& [element_name, elem_info] : element_information_)
2078 {
2079 if (elem_info.Role == element_information::role::element)
2080 {
2082 constituent_order_.push_back(element_name);
2083 }
2084 }
2085
2086 return constituent_order_;
2087}
2088
2090{
2091 // Reset counters and ordering
2093 isotope_order_.clear();
2094
2095 // Collect isotopes marked for inclusion in analysis
2096 for (const auto& [element_name, elem_info] : element_information_)
2097 {
2098 if (elem_info.Role == element_information::role::isotope &&
2099 elem_info.include_in_analysis)
2100 {
2102 isotope_order_.push_back(element_name);
2103 }
2104 }
2105
2106 return isotope_order_;
2107}
2109{
2110 // Reset all counters and ordering vectors
2112 constituent_order_.clear();
2113 element_order_.clear();
2114 size_om_order_.clear();
2115 isotope_order_.clear();
2116
2117 // Pass 1: Collect ALL constituents (elements, isotopes, metadata)
2118 for (const auto& [constituent_name, constituent_info] : element_information_)
2119 {
2121 constituent_order_.push_back(constituent_name);
2122 }
2123
2124 // Pass 2: Collect chemical elements included in analysis
2125 for (const auto& [element_name, elem_info] : element_information_)
2126 {
2127 if (elem_info.Role == element_information::role::element &&
2128 elem_info.include_in_analysis)
2129 {
2130 element_order_.push_back(element_name);
2131 }
2132 }
2133
2134 // Pass 3: Collect isotopes included in analysis
2135 for (const auto& [isotope_name, elem_info] : element_information_)
2136 {
2137 if (elem_info.Role == element_information::role::isotope &&
2138 elem_info.include_in_analysis)
2139 {
2140 isotope_order_.push_back(isotope_name);
2141 }
2142 }
2143
2144 // Pass 4: Collect particle size parameters
2145 for (const auto& [param_name, elem_info] : element_information_)
2146 {
2147 if (elem_info.Role == element_information::role::particle_size)
2148 {
2149 size_om_order_.push_back(param_name);
2150 }
2151 }
2152
2153 // Pass 5: Collect organic carbon parameters
2154 for (const auto& [param_name, elem_info] : element_information_)
2155 {
2156 if (elem_info.Role == element_information::role::organic_carbon)
2157 {
2158 size_om_order_.push_back(param_name);
2159 }
2160 }
2161}
2163{
2164 vector<string> source_names;
2165
2166 // Collect all group names except the target
2167 for (const auto& [group_name, profile_set] : *this)
2168 {
2169 if (group_name != target_group_)
2170 {
2171 source_names.push_back(group_name);
2172 }
2173 }
2174
2175 return source_names;
2176}
2177
2179{
2180 // Search all groups for the specified sample
2181 for (auto& [group_name, profile_set] : *this)
2182 {
2183 // Search within this group's samples
2184 for (const auto& [profile_name, profile] : profile_set)
2185 {
2186 if (profile_name == sample_name)
2187 {
2188 // Found it - return pointer to the profile
2189 return profile_set.GetProfile(profile_name);
2190 }
2191 }
2192 }
2193
2194 // Sample not found in any group
2195 return nullptr;
2196}
2197
2199{
2200 ResultItem result;
2201 Contribution* contributions = new Contribution();
2202
2203 // Package contribution values with source names
2204 vector<string> source_order = GetSourceOrder();
2205 CVector contribution_values = GetContributionVector();
2206
2207 for (size_t i = 0; i < source_order.size(); i++)
2208 {
2209 (*contributions)[source_order[i]] = contribution_values[i];
2210 }
2211
2212 // Configure result item
2213 result.SetName("Contributions");
2214 result.SetResult(contributions);
2216
2217 return result;
2218}
2219
2221{
2222 ResultItem result;
2223 Elemental_Profile* predicted_profile = new Elemental_Profile();
2224
2225 // Compute predicted concentrations using mixing model
2226 CVector predicted_concentrations = PredictTarget(param_mode);
2227 vector<string> element_names = ElementOrder();
2228
2229 // Package predictions into elemental profile
2230 for (size_t i = 0; i < element_names.size(); i++)
2231 {
2232 predicted_profile->AppendElement(element_names[i], predicted_concentrations[i]);
2233 }
2234
2235 // Configure result item
2236 result.SetName("Modeled Elemental Profile");
2237 result.SetResult(predicted_profile);
2239
2240 return result;
2241}
2242
2244{
2245 const size_t num_observations = ObservationsCount();
2246 CVector predicted_values(num_observations);
2247
2248 // Collect predicted value from each observation
2249 for (size_t i = 0; i < num_observations; i++)
2250 {
2251 predicted_values[i] = observation(i)->PredictedValue();
2252 }
2253
2254 return predicted_values;
2255}
2256
2258{
2259 ResultItem result;
2260 Elemental_Profile* predicted_isotopes = new Elemental_Profile();
2261
2262 // Compute predicted delta values using mixing model
2263 CVector predicted_delta_values = PredictTarget_Isotope_delta(param_mode);
2264 vector<string> isotope_names = IsotopeOrder();
2265
2266 // Package predictions into elemental profile
2267 for (size_t i = 0; i < isotope_names.size(); i++)
2268 {
2269 predicted_isotopes->AppendElement(isotope_names[i], predicted_delta_values[i]);
2270 }
2271
2272 // Configure result item
2273 result.SetName("Modeled Elemental Profile for Isotopes");
2274 result.SetResult(predicted_isotopes);
2276
2277 return result;
2278}
2279
2281{
2282 ResultItem result;
2283
2284 // Get predicted and observed profiles
2285 Elemental_Profile* predicted = static_cast<Elemental_Profile*>(
2287 );
2288 Elemental_Profile* observed = static_cast<Elemental_Profile*>(
2290 );
2291
2292 // Create comparison profile set
2293 Elemental_Profile_Set* comparison = new Elemental_Profile_Set();
2294 comparison->AppendProfile("Observed", *observed);
2295 comparison->AppendProfile("Modeled", *predicted);
2296
2297 // Configure result item
2298 result.SetName("Observed vs Modeled Elemental Profile");
2299 result.SetResult(comparison);
2301
2302 return result;
2303}
2304
2306{
2307 vector<ResultItem> mlr_results;
2308
2309 // Collect regression results from each sample group
2310 for (auto& [group_name, profile_set] : *this)
2311 {
2312 // Get MLR models for this group
2313 ResultItem regression_result = profile_set.GetRegressionsAsResult();
2314
2315 // Configure for display
2316 regression_result.SetShowTable(true);
2317 regression_result.SetName("OM & Size MLR for " + group_name);
2318
2319 mlr_results.push_back(regression_result);
2320 }
2321
2322 return mlr_results;
2323}
2324
2326{
2327 ResultItem result;
2328
2329 // Get predicted and observed isotope profiles
2330 Elemental_Profile* predicted = static_cast<Elemental_Profile*>(
2332 );
2333 Elemental_Profile* observed = static_cast<Elemental_Profile*>(
2335 );
2336
2337 // Create comparison profile set
2338 Elemental_Profile_Set* comparison = new Elemental_Profile_Set();
2339 comparison->AppendProfile("Observed", *observed);
2340 comparison->AppendProfile("Modeled", *predicted);
2341
2342 // Configure result item
2343 result.SetName("Observed vs Modeled Elemental Profile for Isotopes");
2344 result.SetResult(comparison);
2346
2347 return result;
2348}
2349
2351{
2352 ResultItem result;
2353 Elemental_Profile* observed_profile = new Elemental_Profile();
2354
2355 // Retrieve observed concentrations for selected target sample
2356 CVector observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_);
2357 vector<string> element_names = ElementOrder();
2358
2359 // Package observations into elemental profile
2360 for (size_t i = 0; i < element_names.size(); i++)
2361 {
2362 observed_profile->AppendElement(element_names[i], observed_concentrations[i]);
2363 }
2364
2365 // Configure result item
2366 result.SetName("Observed Elemental Profile");
2367 result.SetResult(observed_profile);
2369
2370 return result;
2371}
2372
2374{
2375 ResultItem result;
2376 Elemental_Profile* observed_isotopes = new Elemental_Profile();
2377
2378 // Retrieve observed delta values for selected target sample
2380 vector<string> isotope_names = IsotopeOrder();
2381
2382 // Package observations into elemental profile
2383 for (size_t i = 0; i < isotope_names.size(); i++)
2384 {
2385 observed_isotopes->AppendElement(isotope_names[i], observed_delta_values[i]);
2386 }
2387
2388 // Configure result item
2389 result.SetName("Observed Elemental Profile for Isotopes");
2390 result.SetResult(observed_isotopes);
2392
2393 return result;
2394}
2395
2397{
2398 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2399
2400 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2401 {
2402 if (it->first != target_group_)
2403 {
2404 Elemental_Profile element_profile;
2405 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2406 {
2407 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateMean());
2408 }
2409 profile_set->AppendProfile(it->first, element_profile);
2410 }
2411 }
2412
2413 ResultItem resitem;
2414 resitem.SetName("Calculated mean elemental contents");
2416 resitem.SetResult(profile_set);
2417 return resitem;
2418}
2419
2421{
2422 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2423
2424 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2425 {
2426 if (it->first != target_group_)
2427 {
2428 Elemental_Profile element_profile;
2429 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2430 {
2431 if (it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedDistribution()->distribution == distribution_type::normal)
2432 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateStdDev());
2433 else if (it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedDistribution()->distribution == distribution_type::lognormal)
2434 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateStdDevLog());
2435 }
2436 profile_set->AppendProfile(it->first, element_profile);
2437 }
2438 }
2439
2440 ResultItem resitem;
2441 resitem.SetName("Calculated elemental contents standard deviations");
2443 resitem.SetResult(profile_set);
2444 return resitem;
2445}
2446
2447
2449{
2450 vector<ResultItem> source_profile_results;
2451
2452 // Package elemental profiles for each source group
2453 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2454 {
2455 // Skip target group - only process sources
2456 if (it->first != target_group_)
2457 {
2458 // Create and configure result item for this source
2459 ResultItem result_item;
2460 result_item.SetName("Elemental Profiles for " + it->first);
2461 result_item.SetShowAsString(true);
2462 result_item.SetShowTable(true);
2463 result_item.SetShowGraph(true);
2465
2466 // Copy profile set for this source
2467 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2468 *profile_set = it->second;
2469 result_item.SetResult(profile_set);
2470
2471 source_profile_results.push_back(result_item);
2472 }
2473 }
2474
2475 return source_profile_results;
2476}
2477
2478
2480{
2481 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2482
2483 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2484 {
2485 if (it->first != target_group_)
2486 {
2487 Elemental_Profile element_profile;
2488 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2489 {
2490 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateMeanLog());
2491 }
2492 profile_set->AppendProfile(it->first, element_profile);
2493 }
2494 }
2495
2496 ResultItem resitem;
2497 resitem.SetName("Calculated mu parameter of elemental contents");
2499 resitem.SetResult(profile_set);
2500 return resitem;
2501}
2502
2504{
2505 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2506
2507 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2508 {
2509 if (it->first != target_group_)
2510 {
2511 Elemental_Profile element_profile;
2512 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2513 {
2514 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedMu());
2515 }
2516 profile_set->AppendProfile(it->first, element_profile);
2517 }
2518 }
2519
2520 ResultItem resitem;
2521 resitem.SetName("Infered mu parameter elemental contents");
2523 resitem.SetResult(profile_set);
2524 return resitem;
2525}
2526
2528{
2529 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2530
2531 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2532 {
2533 if (it->first != target_group_)
2534 {
2535 Elemental_Profile element_profile;
2536 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2537 {
2538 double sigma = it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedSigma();
2539 double mu = it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedMu();
2540 element_profile.AppendElement(element_order_[element_counter], exp(mu + pow(sigma, 2) / 2));
2541 }
2542 profile_set->AppendProfile(it->first, element_profile);
2543 }
2544 }
2545
2546 ResultItem resitem;
2547 resitem.SetName("Infered mean of elemental contents");
2549 resitem.SetResult(profile_set);
2550 return resitem;
2551}
2552
2554{
2555 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2556
2557 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2558 {
2559 if (it->first != target_group_)
2560 {
2561 Elemental_Profile element_profile;
2562 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2563 {
2564 double sigma = it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedSigma();
2565 element_profile.AppendElement(element_order_[element_counter], sigma);
2566 }
2567 profile_set->AppendProfile(it->first, element_profile);
2568 }
2569 }
2570
2571 ResultItem resitem;
2572 resitem.SetName("Infered sigma parameter");
2574 resitem.SetResult(profile_set);
2575 return resitem;
2576}
2577
2578
2579
2581{
2582 QJsonArray tools_used_json_array;
2583
2584 // Serialize each tool name
2585 for (list<string>::const_iterator it = tools_used_.cbegin(); it != tools_used_.cend(); it++)
2586 {
2587 tools_used_json_array.append(QString::fromStdString(*it));
2588 }
2589
2590 return tools_used_json_array;
2591}
2592
2594{
2595 QJsonObject json_object;
2596
2597 // Serialize each option name-value pair
2598 for (QMap<QString, double>::const_iterator it = options_.cbegin(); it != options_.cend(); it++)
2599 {
2600 json_object[it.key()] = it.value();
2601 }
2602
2603 return json_object;
2604}
2605
2606void SourceSinkData::AddtoToolsUsed(const string& tool)
2607{
2608 // Only add if not already present
2609 if (!ToolsUsed(tool))
2610 {
2611 tools_used_.push_back(tool);
2612 }
2613}
2614
2615bool SourceSinkData::ReadToolsUsedFromJsonObject(const QJsonArray& jsonarray)
2616{
2617 // Deserialize each tool name
2618 foreach (const QJsonValue& value, jsonarray)
2619 {
2620 AddtoToolsUsed(value.toString().toStdString());
2621 }
2622
2623 return true;
2624}
2625
2627{
2628 // Clear existing element information
2629 element_information_.clear();
2630
2631 // Deserialize metadata for each element
2632 for (QString key : jsonobject.keys())
2633 {
2634 element_information elem_info;
2635 elem_info.Role = Role(jsonobject[key].toObject()["Role"].toString());
2636 elem_info.standard_ratio = jsonobject[key].toObject()["Standard Ratio"].toDouble();
2637 elem_info.base_element = jsonobject[key].toObject()["Base Element"].toString().toStdString();
2638 elem_info.include_in_analysis = jsonobject[key].toObject()["Include"].toBool();
2639
2640 element_information_[key.toStdString()] = elem_info;
2641 }
2642
2643 return true;
2644}
2645
2646bool SourceSinkData::ReadElementDatafromJsonObject(const QJsonObject& jsonobject)
2647{
2648 // Clear existing data
2649 clear();
2650
2651 // Deserialize each sample group
2652 for (QString key : jsonobject.keys())
2653 {
2655 elemental_profile_set.ReadFromJsonObject(jsonobject[key].toObject());
2656 operator[](key.toStdString()) = elemental_profile_set;
2657 }
2658
2659 return true;
2660}
2661
2662bool SourceSinkData::ReadOptionsfromJsonObject(const QJsonObject& jsonobject)
2663{
2664 // Deserialize each option
2665 for (QString key : jsonobject.keys())
2666 {
2667 options_[key] = jsonobject[key].toDouble();
2668 }
2669
2670 return true;
2671}
2672
2674{
2675 QJsonObject json_object;
2676
2677 // Serialize each sample group
2678 for (map<string, Elemental_Profile_Set>::const_iterator it = cbegin(); it != cend(); it++)
2679 {
2680 json_object[QString::fromStdString(it->first)] = it->second.toJsonObject();
2681 }
2682
2683 return json_object;
2684}
2685
2687{
2688 // Write header
2689 file->write("***\n");
2690 file->write("Elemental Profiles\n");
2691
2692 // Write each sample group
2693 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2694 {
2695 file->write("**\n");
2696 file->write(QString::fromStdString(it->first + "\n").toUtf8());
2697 it->second.writetofile(file);
2698 }
2699
2700 return true;
2701}
2702
2704{
2705 return WriteDataToFile(file);
2706}
2707
2709{
2710 // Clear existing data
2711 Clear();
2712
2713 // Parse JSON from file
2714 QJsonObject jsondoc = QJsonDocument().fromJson(fil->readAll()).object();
2715
2716 // Load all components
2717 ReadElementDatafromJsonObject(jsondoc["Element Data"].toObject());
2718 ReadElementInformationfromJsonObject(jsondoc["Element Information"].toObject());
2719 ReadToolsUsedFromJsonObject(jsondoc["Tools used"].toArray());
2720 ReadOptionsfromJsonObject(jsondoc["Options"].toObject());
2721 target_group_ = jsondoc["Target Group"].toString().toStdString();
2722
2723 return true;
2724}
2725
2727{
2728 QJsonObject json_object;
2729
2730 // Serialize metadata for each element
2731 for (map<string, element_information>::const_iterator it = element_information_.cbegin(); it != element_information_.cend(); it++)
2732 {
2733 QJsonObject elem_info_json_obj;
2734 elem_info_json_obj["Role"] = Role(it->second.Role);
2735 elem_info_json_obj["Standard Ratio"] = it->second.standard_ratio;
2736 elem_info_json_obj["Base Element"] = QString::fromStdString(it->second.base_element);
2737 elem_info_json_obj["Include"] = it->second.include_in_analysis;
2738
2739 json_object[QString::fromStdString(it->first)] = elem_info_json_obj;
2740 }
2741
2742 return json_object;
2743}
2744
2746{
2747 // Convert enum to string representation
2749 return "DoNotInclude";
2750 else if (role == element_information::role::element)
2751 return "Element";
2752 else if (role == element_information::role::isotope)
2753 return "Isotope";
2755 return "ParticleSize";
2757 return "OM";
2758
2759 // Default for unrecognized values
2760 return "DoNotInclude";
2761}
2762
2763element_information::role SourceSinkData::Role(const QString& role_string) const
2764{
2765 // Convert string to enum representation
2766 if (role_string == "DoNotInclude")
2768 else if (role_string == "Element")
2770 else if (role_string == "Isotope")
2772 else if (role_string == "ParticleSize")
2774 else if (role_string == "OM")
2776
2777 // Default for unrecognized values
2779}
2780
2782 const string& om,
2783 const string& particle_size,
2784 regression_form form,
2785 const double& p_value_threshold)
2786{
2787 // Store OM and size constituent names for later use in corrections
2788 omconstituent_ = om;
2789 sizeconsituent_ = particle_size;
2790 regression_p_value_threshold_ = p_value_threshold;
2791
2792 // Compute regression models for all sample groups
2793 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2794 {
2795 it->second.SetRegressionModels(om, particle_size, form, p_value_threshold);
2796 }
2797
2798 return true;
2799}
2800
2801void SourceSinkData::OutlierAnalysisForAll(const double& lower_threshold, const double& upper_threshold)
2802{
2803 // Perform outlier detection on each source group
2804 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2805 {
2806 // Skip target group - only analyze sources
2807 if (it->first != target_group_)
2808 {
2809 it->second.DetectOutliers(lower_threshold, upper_threshold);
2810 }
2811 }
2812}
2813
2815{
2816 DFA_result result;
2817
2818 // Initialize result vectors for all sources
2819 const size_t num_sources = size() - 1; // Exclude target
2820 result.F_test_P_value = CMBVector(num_sources);
2821 result.p_values = CMBVector(num_sources);
2822 result.wilkslambda = CMBVector(num_sources);
2823
2824 // Perform DFA for each source vs all others
2825 size_t counter = 0;
2826 for (map<string, Elemental_Profile_Set>::iterator source = begin(); source != end(); source++)
2827 {
2828 // Skip target group
2829 if (source->first != target_group_)
2830 {
2831 // Run one-vs-rest DFA for this source
2832 DFA_result source_result = DiscriminantFunctionAnalysis(source->first);
2833
2834 // Extract and store results
2835 result.F_test_P_value[counter] = source_result.F_test_P_value[0];
2836 result.F_test_P_value.SetLabel(counter, source->first);
2837
2838 result.p_values[counter] = source_result.p_values[0];
2839 result.p_values.SetLabel(counter, source->first);
2840
2841 result.wilkslambda[counter] = source_result.wilkslambda[0];
2842 result.wilkslambda.SetLabel(counter, source->first);
2843
2844 result.eigen_vectors.Append(source->first, source_result.eigen_vectors[source->first + " vs the rest"]);
2845 result.multi_projected.Append(source->first, source_result.projected);
2846
2847 counter++;
2848 }
2849 }
2850
2851 return result;
2852}
2853
2854DFA_result SourceSinkData::DiscriminantFunctionAnalysis(const string& source1, const string& source2)
2855{
2856 // Create temporary dataset with only two sources
2857 SourceSinkData two_sources;
2858 two_sources.AppendSampleSet(source1, at(source1));
2859 two_sources.AppendSampleSet(source2, at(source2));
2861
2862 DFA_result result;
2863
2864 // Compute discriminant eigenvector
2865 CMBVector eigen_vector = two_sources.DFA_eigvector();
2866
2867 // Return empty result if eigenvector computation failed
2868 if (eigen_vector.size() == 0)
2869 {
2870 return result;
2871 }
2872
2873 // Project samples onto discriminant axis
2874 result.projected = two_sources.DFA_Projected(source1, source2);
2875
2876 // Compute F-test p-value for separation
2877 result.F_test_P_value = CMBVector(1);
2878 result.F_test_P_value[0] = result.projected.FTest_p_value();
2879
2880 // Store eigenvector
2881 result.eigen_vectors.Append(source1 + " vs " + source2, eigen_vector);
2882
2883 // Compute discriminant p-value and Wilks' Lambda
2884 double p_value = two_sources.DFA_P_Value();
2885 result.p_values = CMBVector(1);
2886 result.p_values[0] = p_value;
2887
2888 result.wilkslambda = CMBVector(1);
2889 result.wilkslambda[0] = two_sources.WilksLambda();
2890
2891 return result;
2892}
2893
2895{
2896 // Create temporary dataset with one source vs all others
2897 SourceSinkData two_sources;
2898 two_sources.AppendSampleSet(source1, at(source1));
2899 two_sources.AppendSampleSet("Others", TheRest(source1));
2901
2902 DFA_result result;
2903
2904 // Compute discriminant eigenvector
2905 CMBVector eigen_vector = two_sources.DFA_eigvector();
2906
2907 // Project all samples onto discriminant axis
2908 result.projected = two_sources.DFA_Projected(source1, this);
2909
2910 // Project pairwise comparison (source vs others)
2911 CMBVectorSet pairwise_projected = two_sources.DFA_Projected(source1, "Others");
2912
2913 // Compute F-test p-value from pairwise projection
2914 result.F_test_P_value = CMBVector(1);
2915 result.F_test_P_value[0] = pairwise_projected.FTest_p_value();
2916
2917 // Store eigenvector with descriptive label
2918 result.eigen_vectors.Append(source1 + " vs the rest", eigen_vector);
2919
2920 // Compute discriminant p-value and Wilks' Lambda
2921 double p_value = two_sources.DFA_P_Value();
2922 result.p_values = CMBVector(1);
2923 result.p_values[0] = p_value;
2924
2925 result.wilkslambda = CMBVector(1);
2926 result.wilkslambda[0] = two_sources.WilksLambda();
2927
2928 return result;
2929}
2931{
2932 int count = 0;
2933
2934 // Sum samples across all source groups
2935 for (map<string, Elemental_Profile_Set>::const_iterator source_group = cbegin(); source_group != cend(); source_group++)
2936 {
2937 // Skip target group - only count source samples
2938 if (source_group->first != target_group_)
2939 {
2940 count += source_group->second.size();
2941 }
2942 }
2943
2944 return count;
2945}
2946
2947CMBVector SourceSinkData::DFATransformed(const CMBVector& eigenvector, const string& source_group)
2948{
2949 // Initialize result vector for discriminant scores
2950 CMBVector discriminant_scores(operator[](source_group).size());
2951
2952 // Project each sample onto the discriminant axis
2953 size_t sample_index = 0;
2954 for (map<string, Elemental_Profile>::iterator sample = operator[](source_group).begin();
2955 sample != operator[](source_group).end();
2956 sample++)
2957 {
2958 // Compute dot product: discriminant score = eigenvector · sample_profile
2959 discriminant_scores[sample_index] = sample->second.CalculateDotProduct(eigenvector);
2960 discriminant_scores.SetLabel(sample_index, sample->first);
2961
2962 sample_index++;
2963 }
2964
2965 return discriminant_scores;
2966}
2967
2969{
2970 Elemental_Profile_Set combined_sources;
2971
2972 // Iterate through all sample groups
2973 for (map<string, Elemental_Profile_Set>::iterator profile_set = begin();
2974 profile_set != end();
2975 profile_set++)
2976 {
2977 // Skip the excluded source and target group
2978 if (profile_set->first != excluded_source && profile_set->first != target_group_)
2979 {
2980 // Add all samples from this source group
2981 for (map<string, Elemental_Profile>::iterator profile = profile_set->second.begin();
2982 profile != profile_set->second.end();
2983 profile++)
2984 {
2985 combined_sources.AppendProfile(profile->first, profile->second);
2986 }
2987 }
2988 }
2989
2990 return combined_sources;
2991}
2992
2993
2994CMBVector SourceSinkData::BracketTest(const string& target_sample, bool correct_based_on_om_n_size)
2995{
2996 vector<string> element_names = GetElementNames();
2997 const size_t num_elements = element_names.size();
2998
2999 // Initialize result vectors
3000 CMBVector bracket_test_results(num_elements);
3001 CVector exceeds_max(num_elements); // Flags for concentrations above source maximum
3002 CVector below_min(num_elements); // Flags for concentrations below source minimum
3003
3004 exceeds_max.SetAllValues(1.0); // Assume all exceed until proven otherwise
3005 below_min.SetAllValues(1.0); // Assume all below until proven otherwise
3006
3007 // Create corrected dataset for testing
3009 false, false, correct_based_on_om_n_size, target_sample);
3010
3011 // Check each source group's concentration ranges
3012 for (map<string, Elemental_Profile_Set>::iterator it = corrected_data.begin();
3013 it != corrected_data.end();
3014 it++)
3015 {
3016 // Skip target group
3017 if (it->first != target_group_)
3018 {
3019 // Check each element
3020 for (size_t i = 0; i < num_elements; i++)
3021 {
3022 bracket_test_results.SetLabel(i, element_names[i]);
3023
3024 double target_concentration = corrected_data.at(target_group_)
3025 .GetProfile(target_sample)->at(element_names[i]);
3026 double source_maximum = it->second.GetElementDistribution(element_names[i])
3027 ->GetMaximum();
3028 double source_minimum = it->second.GetElementDistribution(element_names[i])
3029 ->GetMinimum();
3030
3031 // Check if target is within this source's range
3032 if (target_concentration <= source_maximum)
3033 {
3034 exceeds_max[i] = 0; // Target does not exceed max
3035 }
3036 if (target_concentration >= source_minimum)
3037 {
3038 below_min[i] = 0; // Target is not below min
3039 }
3040 }
3041 }
3042 }
3043
3044 // Compile results and annotate failures
3045 for (size_t i = 0; i < num_elements; i++)
3046 {
3047 // Fail if either above all maxima or below all minima
3048 bracket_test_results[i] = max(exceeds_max[i], below_min[i]);
3049
3050 // Annotate failures in target sample notes
3051 if (exceeds_max[i] > 0.5)
3052 {
3053 at(target_group_).GetProfile(target_sample)->AppendtoNotes(
3054 element_names[i] + " value is higher than the maximum of the sources");
3055 }
3056 else if (below_min[i] > 0.5)
3057 {
3058 at(target_group_).GetProfile(target_sample)->AppendtoNotes(
3059 element_names[i] + " value is lower than the minimum of the sources");
3060 }
3061 }
3062
3063 return bracket_test_results;
3064}
3065
3066
3068 bool correct_based_on_om_n_size,
3069 bool exclude_elements,
3070 bool exclude_samples)
3071{
3072 // Initialize result matrix
3073 // Note: Matrix is constructed as (num_rows, num_cols) but accessed as [row][col]
3074 const size_t num_target_samples = at(target_group_).size();
3075 const size_t num_elements = CountElements(exclude_elements);
3076 CMBMatrix bracket_results(num_target_samples, num_elements);
3077
3078 // Test each target sample
3079 size_t sample_index = 0;
3080 for (map<string, Elemental_Profile>::iterator sample = at(target_group_).begin();
3081 sample != at(target_group_).end();
3082 sample++)
3083 {
3084 // Prepare dataset for this target sample
3085 SourceSinkData corrected_data;
3086 if (correct_based_on_om_n_size)
3087 {
3088 corrected_data = CreateCorrectedAndFilteredDataset(
3089 exclude_samples, exclude_elements, true, sample->first);
3090 }
3091 else
3092 {
3093 corrected_data = CreateCorrectedAndFilteredDataset(
3094 exclude_samples, exclude_elements, false);
3095 }
3096
3097 // Perform bracket test for this sample
3098 vector<string> element_names = corrected_data.GetElementNames();
3099 CMBVector bracket_vector = corrected_data.BracketTest(sample->first, false);
3100
3101 // Store results in matrix
3102 for (size_t element_index = 0; element_index < element_names.size(); element_index++)
3103 {
3104 bracket_results[element_index][sample_index] = bracket_vector.valueAt(element_index);
3105 bracket_results.SetRowLabel(element_index, element_names[element_index]);
3106 }
3107 bracket_results.SetColumnLabel(sample_index, sample->first);
3108
3109 sample_index++;
3110 }
3111
3112 return bracket_results;
3113}
3114
3115
3116
3118{
3119 // Compute optimal lambda parameters for each element
3120 CMBVector lambda_parameters = OptimalBoxCoxParameters();
3121
3122 // Create copy of data for transformation
3123 SourceSinkData transformed_data(*this);
3124
3125 // Apply Box-Cox transformation to each source group
3126 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
3127 {
3128 // Skip target group - only transform sources
3129 if (it->first != target_group_)
3130 {
3131 if (calculate_optimal_lambda)
3132 {
3133 // Transform using optimal lambda parameters
3134 transformed_data[it->first] = it->second.ApplyBoxCoxTransform(&lambda_parameters);
3135 }
3136 else
3137 {
3138 // Transform using default parameters
3139 transformed_data[it->first] = it->second.ApplyBoxCoxTransform();
3140 }
3141 }
3142 }
3143
3144 // Recalculate distributions for transformed data
3145 transformed_data.PopulateElementDistributions();
3146 transformed_data.AssignAllDistributions();
3147
3148 return transformed_data;
3149}
3150
3151map<string, ConcentrationSet> SourceSinkData::ExtractConcentrationSet()
3152{
3153 map<string, ConcentrationSet> element_concentrations;
3154 vector<string> element_names = GetElementNames();
3155
3156 // Build concentration set for each element
3157 for (size_t i = 0; i < element_names.size(); i++)
3158 {
3159 ConcentrationSet concentration_set;
3160
3161 // Collect concentrations from all source groups
3162 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
3163 {
3164 // Skip target group - only collect from sources
3165 if (it->first != target_group_)
3166 {
3167 // Add each sample's concentration for this element
3168 for (map<string, Elemental_Profile>::iterator sample = it->second.begin();
3169 sample != it->second.end();
3170 sample++)
3171 {
3172 concentration_set.AppendValue(sample->second[element_names[i]]);
3173 }
3174 }
3175 }
3176
3177 element_concentrations[element_names[i]] = concentration_set;
3178 }
3179
3180 return element_concentrations;
3181}
3182
3184{
3185 // Extract concentration distributions for all elements
3186 map<string, ConcentrationSet> concentration_sets = ExtractConcentrationSet();
3187 vector<string> element_names = GetElementNames();
3188
3189 // Compute optimal lambda for each element
3190 CMBVector lambda_parameters(element_names.size());
3191
3192 for (size_t i = 0; i < element_names.size(); i++)
3193 {
3194 // Find optimal lambda using maximum likelihood estimation
3195 // Search range: [-5, 5], refinement iterations: 10
3196 lambda_parameters[i] = concentration_sets[element_names[i]].FindOptimalBoxCoxParameter(-5, 5, 10);
3197 }
3198
3199 // Label parameters with element names
3200 lambda_parameters.SetLabels(element_names);
3201
3202 return lambda_parameters;
3203}
3204Elemental_Profile SourceSinkData::t_TestPValue(const string& source1, const string& source2, bool use_log)
3205{
3206 vector<string> element_names = GetElementNames();
3207 Elemental_Profile p_values;
3208
3209 // Perform t-test for each element
3210 for (size_t i = 0; i < element_names.size(); i++)
3211 {
3212 // Get concentration distributions for both sources
3213 ConcentrationSet concentration_set1 = *at(source1).GetElementDistribution(element_names[i]);
3214 ConcentrationSet concentration_set2 = *at(source2).GetElementDistribution(element_names[i]);
3215
3216 // Compute statistics in appropriate space
3217 double std1, std2, mean1, mean2;
3218 if (!use_log)
3219 {
3220 // Linear-space statistics
3221 std1 = concentration_set1.CalculateStdDev();
3222 std2 = concentration_set2.CalculateStdDev();
3223 mean1 = concentration_set1.CalculateMean();
3224 mean2 = concentration_set2.CalculateMean();
3225 }
3226 else
3227 {
3228 // Log-space statistics
3229 std1 = concentration_set1.CalculateStdDevLog();
3230 std2 = concentration_set2.CalculateStdDevLog();
3231 mean1 = concentration_set1.CalculateMeanLog();
3232 mean2 = concentration_set2.CalculateMeanLog();
3233 }
3234
3235 // Compute t-statistic
3236 double standard_error = sqrt(pow(std1, 2) / concentration_set1.size() +
3237 pow(std2, 2) / concentration_set2.size());
3238 double t_statistic = (mean1 - mean2) / standard_error;
3239
3240 // Compute two-tailed p-value
3241 size_t degrees_of_freedom = concentration_set1.size() + concentration_set2.size() - 2;
3242 double p_value_upper = gsl_cdf_tdist_Q(t_statistic, degrees_of_freedom);
3243 double p_value_lower = gsl_cdf_tdist_P(t_statistic, degrees_of_freedom);
3244
3245 // Two-tailed: use minimum of tails, then sum
3246 p_value_upper = min(p_value_upper, 1.0 - p_value_upper);
3247 p_value_lower = min(p_value_lower, 1.0 - p_value_lower);
3248 double p_value = p_value_upper + p_value_lower;
3249
3250 p_values.AppendElement(element_names[i], p_value);
3251 }
3252
3253 return p_values;
3254}
3255
3256Elemental_Profile SourceSinkData::DifferentiationPower(const string& source1, const string& source2, bool use_log)
3257{
3258 vector<string> element_names = GetElementNames();
3259 Elemental_Profile differentiation_powers;
3260
3261 // Compute differentiation power for each element
3262 for (size_t i = 0; i < element_names.size(); i++)
3263 {
3264 // Get concentration distributions for both sources
3265 ConcentrationSet concentration_set1 = *at(source1).GetElementDistribution(element_names[i]);
3266 ConcentrationSet concentration_set2 = *at(source2).GetElementDistribution(element_names[i]);
3267
3268 // Compute statistics in appropriate space
3269 double std1, std2, mean1, mean2;
3270 if (!use_log)
3271 {
3272 // Linear-space statistics
3273 std1 = concentration_set1.CalculateStdDev();
3274 std2 = concentration_set2.CalculateStdDev();
3275 mean1 = concentration_set1.CalculateMean();
3276 mean2 = concentration_set2.CalculateMean();
3277 }
3278 else
3279 {
3280 // Log-space statistics
3281 std1 = concentration_set1.CalculateStdDevLog();
3282 std2 = concentration_set2.CalculateStdDevLog();
3283 mean1 = concentration_set1.CalculateMeanLog();
3284 mean2 = concentration_set2.CalculateMeanLog();
3285 }
3286
3287 // Compute differentiation power: 2 × |Δμ| / (σ₁ + σ₂)
3288 double diff_power = 2.0 * fabs(mean1 - mean2) / (std1 + std2);
3289
3290 differentiation_powers.AppendElement(element_names[i], diff_power);
3291 }
3292
3293 return differentiation_powers;
3294}
3295
3297{
3298 Elemental_Profile_Set all_pairs;
3299
3300 // Compare all pairs of source groups
3301 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3302 {
3303 if (include_target || it->first != target_group_)
3304 {
3305 for (map<string, Elemental_Profile_Set>::iterator it2 = next(it); it2 != end(); it2++)
3306 {
3307 if (include_target || it2->first != target_group_)
3308 {
3309 Elemental_Profile diff_power = DifferentiationPower(it->first, it2->first, use_log);
3310 all_pairs.AppendProfile(it->first + " and " + it2->first, diff_power);
3311 }
3312 }
3313 }
3314 }
3315
3316 return all_pairs;
3317}
3318
3319Elemental_Profile SourceSinkData::DifferentiationPower_Percentage(const string& source1, const string& source2)
3320{
3321 vector<string> element_names = GetElementNames();
3322 Elemental_Profile classification_percentages;
3323
3324 // Compute classification percentage for each element
3325 for (size_t i = 0; i < element_names.size(); i++)
3326 {
3327 // Get concentration distributions
3328 ConcentrationSet concentration_set1 = *at(source1).GetElementDistribution(element_names[i]);
3329 ConcentrationSet concentration_set2 = *at(source2).GetElementDistribution(element_names[i]);
3330
3331 // Pool all samples and compute ranks
3332 ConcentrationSet combined_set = concentration_set1;
3333 combined_set.AppendSet(concentration_set2);
3334 vector<unsigned int> ranks = combined_set.CalculateRanks();
3335
3336 // Count correct classifications
3337 size_t n1 = concentration_set1.size();
3338 int set1_below_limit = aquiutils::CountLessThan(ranks, n1, n1, false);
3339 int set1_above_limit = aquiutils::CountGreaterThan(ranks, n1, n1, false);
3340 int set2_below_limit = aquiutils::CountLessThan(ranks, n1, n1, true);
3341 int set2_above_limit = aquiutils::CountGreaterThan(ranks, n1, n1, true);
3342
3343 // Compute classification success rates
3344 double set1_fraction = double(set1_below_limit + set2_above_limit) / double(combined_set.size());
3345 double set2_fraction = double(set1_above_limit + set2_below_limit) / double(combined_set.size());
3346
3347 // Use best classification direction
3348 double classification_percentage = max(set1_fraction, set2_fraction);
3349
3350 classification_percentages.AppendElement(element_names[i], classification_percentage);
3351 }
3352
3353 return classification_percentages;
3354}
3355
3357{
3358 Elemental_Profile_Set all_pairs;
3359
3360 // Compare all pairs of source groups
3361 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3362 {
3363 if (include_target || it->first != target_group_)
3364 {
3365 for (map<string, Elemental_Profile_Set>::iterator it2 = next(it); it2 != end(); it2++)
3366 {
3367 if (include_target || it2->first != target_group_)
3368 {
3369 Elemental_Profile diff_power_pct = DifferentiationPower_Percentage(it->first, it2->first);
3370 all_pairs.AppendProfile(it->first + " and " + it2->first, diff_power_pct);
3371 }
3372 }
3373 }
3374 }
3375
3376 return all_pairs;
3377}
3378
3380{
3381 Elemental_Profile_Set all_pairs;
3382
3383 // Compare all pairs of source groups
3384 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3385 {
3386 if (include_target || it->first != target_group_)
3387 {
3388 for (map<string, Elemental_Profile_Set>::iterator it2 = next(it); it2 != end(); it2++)
3389 {
3390 if (include_target || it2->first != target_group_)
3391 {
3392 Elemental_Profile p_values = t_TestPValue(it->first, it2->first, false);
3393 all_pairs.AppendProfile(it->first + " and " + it2->first, p_values);
3394 }
3395 }
3396 }
3397 }
3398
3399 return all_pairs;
3400}
3401
3403{
3404 vector<string> error_messages;
3405
3406 // Ensure element ordering is current
3408
3409 // Check each source group for negative values
3410 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3411 {
3412 vector<string> negative_elements = it->second.CheckForNegativeValues(element_order_);
3413
3414 // Generate error message for each problematic element
3415 for (size_t i = 0; i < negative_elements.size(); i++)
3416 {
3417 error_messages.push_back("There are zero or negative values for element '" +
3418 negative_elements[i] + "' in sample group '" +
3419 it->first + "'");
3420 }
3421 }
3422
3423 return error_messages;
3424}
3425
3426void SourceSinkData::IncludeExcludeAllElements(bool include_in_analysis)
3427{
3428 // Set inclusion flag for all elements
3429 for (map<string, element_information>::iterator it = element_information_.begin();
3430 it != element_information_.end();
3431 it++)
3432 {
3433 it->second.include_in_analysis = include_in_analysis;
3434 }
3435}
3436
3437double SourceSinkData::GrandMean(const string& element, bool use_log)
3438{
3439 double weighted_sum = 0.0;
3440 double total_count = 0.0;
3441
3442 // Accumulate weighted means from each source group
3443 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3444 {
3445 // Skip target group
3446 if (it->first != target_group_)
3447 {
3448 ConcentrationSet* element_dist = it->second.GetElementDistribution(element);
3449 size_t sample_count = element_dist->size();
3450
3451 if (!use_log)
3452 {
3453 // Linear-space mean
3454 weighted_sum += element_dist->CalculateMean() * sample_count;
3455 }
3456 else
3457 {
3458 // Log-space mean
3459 weighted_sum += element_dist->CalculateMeanLog() * sample_count;
3460 }
3461
3462 total_count += sample_count;
3463 }
3464 }
3465
3466 // Compute weighted average
3467 return weighted_sum / total_count;
3468}
3469
3471{
3472 Elemental_Profile_Set combined_sources;
3473
3474 // Pool samples from all source groups
3475 for (map<string, Elemental_Profile_Set>::const_iterator it = begin(); it != prev(end()); it++)
3476 {
3477 // Skip target group
3478 if (it->first != target_group_)
3479 {
3480 combined_sources.AppendProfiles(it->second, nullptr);
3481 }
3482 }
3483
3484 // Recalculate distributions for combined dataset
3485 combined_sources.UpdateElementDistributions();
3486
3487 return combined_sources;
3488}
3489
3491{
3492 vector<string> element_names = GetElementNames();
3493 CMBVector p_values(element_names.size());
3494 p_values.SetLabels(element_names);
3495
3496 // Perform ANOVA for each element
3497 for (size_t i = 0; i < element_names.size(); i++)
3498 {
3499 ANOVA_info anova_result = ANOVA(element_names[i], use_log);
3500 p_values[i] = anova_result.p_value;
3501 }
3502
3503 return p_values;
3504}
3505
3506ANOVA_info SourceSinkData::ANOVA(const string& element, bool use_log)
3507{
3508 ANOVA_info anova;
3509
3510 // Combine all source samples for total variance calculation
3512 ConcentrationSet* all_element_data = all_sources.GetElementDistribution(element);
3513
3514 if (!use_log)
3515 {
3516 // Linear-space ANOVA
3517
3518 // Total sum of squares (SST)
3519 anova.SST = all_element_data->CalculateSSE();
3520
3521 // Between-group sum of squares (SSB) and within-group sum of squares (SSW)
3522 double grand_mean = all_element_data->CalculateMean();
3523 double sum_between = 0.0;
3524 double sum_within = 0.0;
3525
3526 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
3527 source_group != end();
3528 source_group++)
3529 {
3530 if (source_group->first != target_group_)
3531 {
3532 ConcentrationSet* group_data = source_group->second.GetElementDistribution(element);
3533 double group_mean = group_data->CalculateMean();
3534 size_t group_size = group_data->size();
3535
3536 // SSB: variance of group means weighted by sample size
3537 sum_between += pow(group_mean - grand_mean, 2) * group_size;
3538
3539 // SSW: sum of within-group variances
3540 sum_within += group_data->CalculateSSE();
3541 }
3542 }
3543
3544 anova.SSB = sum_between;
3545 anova.SSW = sum_within;
3546 }
3547 else
3548 {
3549 // Log-space ANOVA
3550
3551 // Total sum of squares (SST)
3552 double total_std_log = all_element_data->CalculateStdDevLog();
3553 size_t total_size = all_element_data->size();
3554 anova.SST = pow(total_std_log, 2) * (total_size - 1);
3555
3556 // Between-group sum of squares (SSB) and within-group sum of squares (SSW)
3557 double grand_mean_log = all_element_data->CalculateMeanLog();
3558 double sum_between = 0.0;
3559 double sum_within = 0.0;
3560
3561 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
3562 source_group != end();
3563 source_group++)
3564 {
3565 if (source_group->first != target_group_)
3566 {
3567 ConcentrationSet* group_data = source_group->second.GetElementDistribution(element);
3568 double group_mean_log = group_data->CalculateMeanLog();
3569 size_t group_size = group_data->size();
3570
3571 // SSB: variance of group means weighted by sample size
3572 sum_between += pow(group_mean_log - grand_mean_log, 2) * group_size;
3573
3574 // SSW: sum of within-group variances
3575 sum_within += group_data->CalculateSSELog();
3576 }
3577 }
3578
3579 anova.SSB = sum_between;
3580 anova.SSW = sum_within;
3581 }
3582
3583 // Compute mean squares and F-statistic
3584 size_t num_groups = this->size() - 1; // Exclude target
3585 size_t total_samples = all_element_data->size();
3586
3587 // Degrees of freedom
3588 size_t df_between = num_groups - 1;
3589 size_t df_within = total_samples - num_groups;
3590
3591 // Mean squares
3592 anova.MSB = anova.SSB / double(df_between);
3593 anova.MSW = anova.SSW / double(df_within);
3594
3595 // F-statistic
3596 anova.F = anova.MSB / anova.MSW;
3597
3598 // P-value from F-distribution
3599 anova.p_value = gsl_cdf_fdist_Q(anova.F, df_between, total_samples);
3600
3601 return anova;
3602}
3603void SourceSinkData::IncludeExcludeElementsBasedOn(const vector<string>& elements)
3604{
3605 // First, exclude all elements
3606 for (map<string, element_information>::iterator element = element_information_.begin();
3607 element != element_information_.end();
3608 element++)
3609 {
3610 element->second.include_in_analysis = false;
3611 }
3612
3613 // Then, include only specified elements
3614 for (size_t i = 0; i < elements.size(); i++)
3615 {
3616 if (element_information_.count(elements[i]) > 0)
3617 {
3618 element_information_[elements[i]].include_in_analysis = true;
3619 }
3620 }
3621}
3622
3624{
3625 SourceSinkData reduced_dataset;
3626
3627 // Randomly select samples to eliminate
3628 vector<string> samples_to_eliminate = RandomlypickSamples(percentage / 100.0);
3629
3630 // Copy groups with specified samples eliminated
3631 for (map<string, Elemental_Profile_Set>::const_iterator it = cbegin(); it != cend(); it++)
3632 {
3633 if (it->first != target_group_)
3634 {
3635 // Remove randomly selected samples from source groups
3636 reduced_dataset[it->first] = it->second.EliminateSamples(
3637 samples_to_eliminate, &element_information_);
3638 }
3639 else
3640 {
3641 // Preserve target group unchanged
3642 reduced_dataset[it->first] = it->second;
3643 }
3644 }
3645
3646 // Copy metadata and settings
3647 reduced_dataset.omconstituent_ = omconstituent_;
3648 reduced_dataset.sizeconsituent_ = sizeconsituent_;
3649 reduced_dataset.target_group_ = target_group_;
3650
3651 // Recalculate distributions for reduced dataset
3653 reduced_dataset.PopulateElementDistributions();
3654 reduced_dataset.AssignAllDistributions();
3655
3656 return reduced_dataset;
3657}
3658
3659Elemental_Profile SourceSinkData::Sample(const string& sample_name) const
3660{
3661 // Search all groups for the specified sample
3662 for (map<string, Elemental_Profile_Set>::const_iterator it = cbegin(); it != cend(); it++)
3663 {
3664 if (it->second.count(sample_name) == 1)
3665 {
3666 return it->second.at(sample_name);
3667 }
3668 }
3669
3670 // Sample not found - return empty profile
3671 return Elemental_Profile();
3672}
3673SourceSinkData SourceSinkData::ReplaceSourceAsTarget(const string& source_sample_name) const
3674{
3675 // Create copy of current dataset
3676 SourceSinkData modified_dataset = *this;
3677
3678 // Extract specified source sample
3679 Elemental_Profile target_profile = Sample(source_sample_name);
3680
3681 // Create new target group with this sample
3682 Elemental_Profile_Set new_target_group = Elemental_Profile_Set();
3683 new_target_group.AppendProfile(source_sample_name, target_profile);
3684 modified_dataset[target_group_] = new_target_group;
3685
3686 // Copy metadata and settings
3687 modified_dataset.omconstituent_ = omconstituent_;
3688 modified_dataset.sizeconsituent_ = sizeconsituent_;
3689 modified_dataset.target_group_ = target_group_;
3690
3691 // Recalculate distributions
3693 modified_dataset.PopulateElementDistributions();
3694 modified_dataset.AssignAllDistributions();
3695
3696 return modified_dataset;
3697}
3698
3700 const double& percentage,
3701 unsigned int num_iterations,
3702 string target_sample,
3703 bool use_softmax)
3704{
3705 // Initialize result time series
3706 CMBTimeSeriesSet contribution_time_series(numberofsourcesamplesets_);
3707
3708 // Set source names for time series
3709 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3710 {
3711 contribution_time_series.setname(source_idx, samplesetsorder_[source_idx]);
3712 }
3713
3714 // Perform bootstrap iterations
3715 for (size_t iteration = 0; iteration < num_iterations; iteration++)
3716 {
3717 // Create bootstrapped dataset with randomly excluded samples
3718 SourceSinkData bootstrapped_data = RandomlyEliminateSourceSamples(percentage);
3719 bootstrapped_data.InitializeParametersAndObservations(target_sample);
3720
3721 // Update progress if progress tracker available
3722 if (rtw_)
3723 {
3724 rtw_->SetProgress(double(iteration) / double(num_iterations));
3725 }
3726
3727 // Solve CMB model with appropriate transformation
3728 if (use_softmax)
3729 {
3731 }
3732 else
3733 {
3735 }
3736
3737 // Record contributions from this iteration
3738 contribution_time_series.append(iteration, bootstrapped_data.GetContributionVector().vec);
3739 }
3740
3741 return contribution_time_series;
3742}
3743
3745 Results* results,
3746 const double& percentage,
3747 unsigned int num_iterations,
3748 string target_sample,
3749 bool use_softmax)
3750{
3751 // Initialize contribution time series
3753
3754 // Set source names
3755 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3756 {
3757 contributions->setname(source_idx, samplesetsorder_[source_idx]);
3758 }
3759
3760 // Perform bootstrap iterations
3761 for (size_t iteration = 0; iteration < num_iterations; iteration++)
3762 {
3763 // Create bootstrapped dataset
3764 SourceSinkData bootstrapped_data = RandomlyEliminateSourceSamples(percentage);
3765 bootstrapped_data.InitializeParametersAndObservations(target_sample);
3766
3767 // Update progress
3768 if (rtw_)
3769 {
3770 rtw_->SetProgress(double(iteration) / double(num_iterations));
3771 }
3772
3773 // Solve CMB model
3774 if (use_softmax)
3775 {
3777 }
3778 else
3779 {
3781 }
3782
3783 // Record contributions
3784 contributions->append(iteration, bootstrapped_data.GetContributionVector().vec);
3785 }
3786
3787 // ========== Result 1: Contribution Time Series ==========
3788 ResultItem contributions_result;
3789 results->SetName("Error Analysis for target sample '" + target_sample + "'");
3790 contributions_result.SetName("Error Analysis");
3791 contributions_result.SetResult(contributions);
3792 contributions_result.SetType(result_type::stacked_bar_chart);
3793 contributions_result.SetShowAsString(false);
3794 contributions_result.SetShowTable(true);
3795
3796 // Only show graph for small sample sizes (to avoid clutter)
3797 if (num_iterations < 101)
3798 {
3799 contributions_result.SetShowGraph(true);
3800 }
3801 else
3802 {
3803 contributions_result.SetShowGraph(false);
3804 }
3805
3806 contributions_result.SetYLimit(_range::high, 1);
3807 contributions_result.SetYLimit(_range::low, 0);
3808 contributions_result.SetXAxisMode(xaxis_mode::counter);
3809 contributions_result.setYAxisTitle("Contribution");
3810 contributions_result.setXAxisTitle("Sample");
3811 results->Append(contributions_result);
3812
3813 // ========== Result 2: Posterior Distributions ==========
3814 CMBTimeSeriesSet* distributions = new CMBTimeSeriesSet();
3815 *distributions = contributions->distribution(100, 0, 0);
3816
3817 ResultItem distributions_result;
3818 distributions_result.SetName("Posterior Distributions");
3819 distributions_result.SetShowAsString(false);
3820 distributions_result.SetShowTable(true);
3821 distributions_result.SetType(result_type::distribution);
3822 distributions_result.SetResult(distributions);
3823 results->Append(distributions_result);
3824
3825 // ========== Result 3: 95% Credible Intervals ==========
3826 RangeSet* credible_intervals = new RangeSet();
3827
3828 for (size_t i = 0; i < GetSourceOrder().size(); i++)
3829 {
3830 Range interval;
3831
3832 // Compute statistics for this source
3833 double percentile_2_5 = contributions->at(i).percentile(0.025);
3834 double percentile_97_5 = contributions->at(i).percentile(0.975);
3835 double mean_contribution = contributions->at(i).mean();
3836 double median_contribution = contributions->at(i).percentile(0.5);
3837
3838 // Configure credible interval
3839 interval.Set(_range::low, percentile_2_5);
3840 interval.Set(_range::high, percentile_97_5);
3841 interval.SetMean(mean_contribution);
3842 interval.SetMedian(median_contribution);
3843
3844 (*credible_intervals)[contributions->getSeriesName(i)] = interval;
3845 }
3846
3847 ResultItem intervals_result;
3848 intervals_result.SetName("Source Contribution Credible Intervals");
3849 intervals_result.SetShowAsString(true);
3850 intervals_result.SetShowTable(true);
3851 intervals_result.SetType(result_type::rangeset);
3852 intervals_result.SetResult(credible_intervals);
3853 intervals_result.SetYAxisMode(yaxis_mode::normal);
3854 intervals_result.SetYLimit(_range::high, 1.0);
3855 intervals_result.SetYLimit(_range::low, 0);
3856 results->Append(intervals_result);
3857
3858 return true;
3859}
3860
3862 const string& source_group,
3863 bool use_softmax,
3864 bool apply_om_size_correction)
3865{
3866 // Initialize with first target sample (will be replaced in loop)
3868
3869 // Initialize result time series
3870 CMBTimeSeriesSet validation_results(numberofsourcesamplesets_);
3871
3872 // Set source names
3873 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3874 {
3875 validation_results.setname(source_idx, samplesetsorder_[source_idx]);
3876 }
3877
3878 // Perform leave-one-out validation for each sample in the source group
3879 size_t valid_sample_count = 0;
3880 for (map<string, Elemental_Profile>::iterator sample = at(source_group).begin();
3881 sample != at(source_group).end();
3882 sample++)
3883 {
3884 // Create dataset with this sample as target
3885 SourceSinkData test_dataset = ReplaceSourceAsTarget(sample->first);
3886
3887 // Apply corrections if requested
3888 SourceSinkData corrected_dataset = test_dataset.CreateCorrectedDataset(
3889 sample->first, apply_om_size_correction, test_dataset.GetElementInformation());
3890 corrected_dataset.SetProgressWindow(rtw_);
3891
3892 // Check for negative values (would cause problems in CMB)
3893 vector<string> negative_elements = corrected_dataset.NegativeValueCheck();
3894
3895 if (negative_elements.size() == 0)
3896 {
3897 // No negative values - proceed with CMB solution
3898 test_dataset.InitializeParametersAndObservations(sample->first);
3899
3900 // Update progress
3901 if (rtw_)
3902 {
3903 rtw_->SetProgress(double(valid_sample_count) / double(at(source_group).size()));
3904 }
3905
3906 // Solve CMB model
3907 if (use_softmax)
3908 {
3910 }
3911 else
3912 {
3914 }
3915
3916 // Record contributions and label with sample name
3917 validation_results.append(valid_sample_count, test_dataset.GetContributionVector().vec);
3918 validation_results.SetLabel(valid_sample_count, sample->first);
3919 valid_sample_count++;
3920 }
3921 else
3922 {
3923 // Negative values detected - skip this sample and report
3924 for (size_t i = 0; i < negative_elements.size(); i++)
3925 {
3926 qDebug() << QString::fromStdString(negative_elements[i]);
3927 }
3928 }
3929 }
3930
3931 return validation_results;
3932}
3933
3935 transformation transform,
3936 bool apply_om_size_correction,
3937 map<string, vector<string>>& negative_elements)
3938{
3939 // Initialize with first target sample (will be replaced in loop)
3941
3942 // Initialize result time series
3943 CMBTimeSeriesSet contribution_results(numberofsourcesamplesets_);
3944
3945 // Set source names
3946 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3947 {
3948 contribution_results.setname(source_idx, samplesetsorder_[source_idx]);
3949 }
3950
3951 // Solve CMB for each target sample
3952 size_t valid_sample_count = 0;
3953 for (map<string, Elemental_Profile>::iterator sample = at(target_group_).begin();
3954 sample != at(target_group_).end();
3955 sample++)
3956 {
3957 // Skip samples with empty names
3958 if (sample->first != "")
3959 {
3960 // Apply corrections if requested
3961 SourceSinkData corrected_dataset = CreateCorrectedDataset(
3962 sample->first, apply_om_size_correction, GetElementInformation());
3963
3964 // Check for negative values
3965 negative_elements[sample->first] = corrected_dataset.NegativeValueCheck();
3966
3967 if (negative_elements[sample->first].size() == 0)
3968 {
3969 // No negative values - proceed with CMB solution
3970 corrected_dataset.InitializeParametersAndObservations(sample->first);
3971
3972 // Update progress with sample name
3973 if (rtw_)
3974 {
3975 rtw_->SetProgress(double(valid_sample_count) / double(at(target_group_).size()));
3976 rtw_->SetLabel(QString::fromStdString(sample->first));
3977 }
3978
3979 // Solve CMB model
3980 corrected_dataset.SolveLevenberg_Marquardt(transform);
3981
3982 // Record contributions and label with sample name
3983 contribution_results.append(valid_sample_count, corrected_dataset.GetContributionVector().vec);
3984 contribution_results.SetLabel(valid_sample_count, sample->first);
3985 valid_sample_count++;
3986 }
3987 }
3988 }
3989
3990 // Set progress to 100% at completion
3991 if (rtw_)
3992 {
3993 rtw_->SetProgress(1.0);
3994 }
3995
3996 return contribution_results;
3997}
3998
3999
4001{
4002 vector<string> all_sample_names;
4003
4004 // Collect sample names from all source groups
4005 for (map<string, Elemental_Profile_Set>::const_iterator profile_set = cbegin();
4006 profile_set != cend();
4007 profile_set++)
4008 {
4009 // Skip target group
4010 if (profile_set->first != target_group_)
4011 {
4012 // Add all sample names from this source group
4013 for (map<string, Elemental_Profile>::const_iterator profile = profile_set->second.cbegin();
4014 profile != profile_set->second.cend();
4015 profile++)
4016 {
4017 all_sample_names.push_back(profile->first);
4018 }
4019 }
4020 }
4021
4022 return all_sample_names;
4023}
4024
4025vector<string> SourceSinkData::RandomlypickSamples(const double& percentage) const
4026{
4027 // Get all source sample names
4028 vector<string> all_samples = AllSourceSampleNames();
4029 vector<string> selected_samples;
4030
4031 // Perform Bernoulli sampling: each sample included independently with probability = percentage
4032 for (size_t i = 0; i < all_samples.size(); i++)
4033 {
4034 // Generate random number in [0, 1)
4035 double random_value = GADistribution::GetRndUniF(0, 1);
4036
4037 // Include sample if random value < percentage
4038 if (random_value < percentage)
4039 {
4040 selected_samples.push_back(all_samples[i]);
4041 }
4042 }
4043
4044 return selected_samples;
4045}
4046
4048 const string& target_sample,
4049 map<string, string> arguments,
4051 ProgressWindow* progress_window,
4052 const string& working_folder)
4053{
4054 // Initialize results container
4055 Results results;
4056 results.SetName("MCMC results for '" + target_sample + "'");
4057
4058 // Parse OM/size correction setting
4059 bool apply_om_size_correction = (arguments["Apply size and organic matter correction"] == "true");
4060
4061 // Apply corrections and validate
4062 SourceSinkData corrected_data = CreateCorrectedDataset(
4063 target_sample, apply_om_size_correction, GetElementInformation());
4064 vector<string> negative_elements = corrected_data.NegativeValueCheck();
4065
4066 if (negative_elements.size() > 0)
4067 {
4068 // Negative values detected - return error
4069 results.SetError("Negative elemental content in ");
4070 for (size_t i = 0; i < negative_elements.size(); i++)
4071 {
4072 if (i == 0)
4073 results.AppendError(negative_elements[i]);
4074 else
4075 results.AppendError("," + negative_elements[i]);
4076 }
4077 return results;
4078 }
4079
4080 // Configure MCMC sampler
4081 mcmc->Model = &corrected_data;
4082
4083 // Setup progress window charts
4084 progress_window->SetTitle("Acceptance Rate", 0);
4085 progress_window->SetTitle("Purturbation Factor", 1);
4086 progress_window->SetTitle("Log posterior value", 2);
4087 progress_window->SetYAxisTitle("Acceptance Rate", 0);
4088 progress_window->SetYAxisTitle("Purturbation Factor", 1);
4089 progress_window->SetYAxisTitle("Log posterior value", 2);
4090 progress_window->show();
4091
4092 // ========== Run MCMC Sampling ==========
4093 corrected_data.InitializeParametersAndObservations(target_sample);
4094
4095 // Set MCMC parameters
4096 mcmc->SetProperty("number_of_samples", arguments["Number of samples"]);
4097 mcmc->SetProperty("number_of_chains", arguments["Number of chains"]);
4098 mcmc->SetProperty("number_of_burnout_samples", arguments["Samples to be discarded (burnout)"]);
4099 mcmc->SetProperty("dissolve_chains", arguments["Dissolve Chains"]);
4100
4101 // Initialize MCMC sampler
4103 mcmc->initialize(mcmc_samples, true);
4104
4105 // Determine output file path
4106 string output_path;
4107 if (!QString::fromStdString(arguments["Samples File Name"]).contains("/"))
4108 {
4109 output_path = working_folder + "/";
4110 }
4111
4112 // Run MCMC chains
4113 int num_chains = QString::fromStdString(arguments["Number of chains"]).toInt();
4114 int num_samples = QString::fromStdString(arguments["Number of samples"]).toInt();
4115 mcmc->step(num_chains, num_samples, output_path + arguments["Samples File Name"],
4116 mcmc_samples, progress_window);
4117
4118 // Append last contribution (computed from sum constraint)
4119 vector<string> source_names = corrected_data.SourceGroupNames();
4120 size_t last_source_idx = source_names.size() - 1;
4121 mcmc_samples->AppendLastContribution(last_source_idx,
4122 source_names[last_source_idx] + "_contribution");
4123
4124 // Store MCMC samples result
4125 ResultItem mcmc_samples_result;
4126 mcmc_samples_result.SetShowAsString(false);
4127 mcmc_samples_result.SetType(result_type::mcmc_samples);
4128 mcmc_samples_result.SetName("MCMC samples");
4129 mcmc_samples_result.SetResult(mcmc_samples);
4130 results.Append(mcmc_samples_result);
4131
4132 // Parse burnin parameter
4133 int burnin_samples = QString::fromStdString(arguments["Samples to be discarded (burnout)"]).toInt();
4134
4135 // ========== Result 1: Posterior Distributions for Contributions ==========
4136 ResultItem posterior_distributions_result;
4137 CMBTimeSeriesSet* posterior_distributions = new CMBTimeSeriesSet();
4138 *posterior_distributions = mcmc_samples->distribution(100, burnin_samples);
4139
4140 posterior_distributions_result.SetName("Posterior Distributions");
4141 posterior_distributions_result.SetShowAsString(false);
4142 posterior_distributions_result.SetShowTable(true);
4143 posterior_distributions_result.SetType(result_type::distribution);
4144 posterior_distributions_result.SetResult(posterior_distributions);
4145 results.Append(posterior_distributions_result);
4146
4147 // ========== Result 2: Contribution Credible Intervals ==========
4148 RangeSet* contribution_credible_intervals = new RangeSet();
4149
4150 for (size_t i = 0; i < corrected_data.GetSourceOrder().size(); i++)
4151 {
4152 Range interval;
4153
4154 // Compute statistics (excluding burnin)
4155 double percentile_2_5 = mcmc_samples->at(i).percentile(0.025, burnin_samples);
4156 double percentile_97_5 = mcmc_samples->at(i).percentile(0.975, burnin_samples);
4157 double mean_contribution = mcmc_samples->at(i).mean(burnin_samples);
4158 double median_contribution = mcmc_samples->at(i).percentile(0.5, burnin_samples);
4159
4160 interval.Set(_range::low, percentile_2_5);
4161 interval.Set(_range::high, percentile_97_5);
4162 interval.SetMean(mean_contribution);
4163 interval.SetMedian(median_contribution);
4164
4165 (*contribution_credible_intervals)[mcmc_samples->getSeriesName(i)] = interval;
4166 }
4167
4168 ResultItem contribution_intervals_result;
4169 contribution_intervals_result.SetName("Source Contribution Credible Intervals");
4170 contribution_intervals_result.SetShowAsString(true);
4171 contribution_intervals_result.SetShowTable(true);
4172 contribution_intervals_result.SetType(result_type::rangeset);
4173 contribution_intervals_result.SetResult(contribution_credible_intervals);
4174 contribution_intervals_result.SetYAxisMode(yaxis_mode::log);
4175 contribution_intervals_result.SetYLimit(_range::high, 1.0);
4176 results.Append(contribution_intervals_result);
4177
4178 // ========== Result 3: Predicted Element Distributions ==========
4179 CMBTimeSeriesSet predicted_samples = mcmc->predicted;
4180 vector<string> element_names = corrected_data.ElementOrder();
4181 vector<string> isotope_names = corrected_data.IsotopeOrder();
4182 vector<string> all_constituent_names = element_names;
4183 all_constituent_names.insert(all_constituent_names.end(), isotope_names.begin(), isotope_names.end());
4184
4185 // Set constituent names
4186 for (size_t i = 0; i < predicted_samples.size(); i++)
4187 {
4188 predicted_samples.setname(i, all_constituent_names[i]);
4189 }
4190
4191 // Extract element predictions only
4192 CMBTimeSeriesSet predicted_elements;
4193 for (size_t i = 0; i < predicted_samples.size(); i++)
4194 {
4195 if (corrected_data.GetElementInformation(predicted_samples.getSeriesName(i))->Role ==
4197 {
4198 predicted_elements.append(predicted_samples[i], predicted_samples.getSeriesName(i));
4199 }
4200 }
4201
4202 // Compute posterior distributions for elements
4203 CMBTimeSeriesSet* predicted_element_distributions = new CMBTimeSeriesSet();
4204 *predicted_element_distributions = predicted_elements.distribution(100, burnin_samples);
4205
4206 // Add observed values
4207 for (size_t i = 0; i < predicted_elements.size(); i++)
4208 {
4209 predicted_element_distributions->SetObservedValue(i, corrected_data.observation(i)->Value());
4210 }
4211
4212 ResultItem predicted_elements_result;
4213 predicted_elements_result.SetName("Posterior Predicted Constituents");
4214 predicted_elements_result.SetShowAsString(false);
4215 predicted_elements_result.SetShowTable(true);
4216 predicted_elements_result.SetType(result_type::distribution_with_observed);
4217 predicted_elements_result.SetResult(predicted_element_distributions);
4218 results.Append(predicted_elements_result);
4219
4220 // ========== Result 4: Element Credible Intervals ==========
4221 RangeSet* predicted_element_intervals = new RangeSet();
4222
4223 // Compute percentiles for all predictions
4224 vector<double> percentile_2_5_all = predicted_samples.percentile(0.025, burnin_samples);
4225 vector<double> percentile_97_5_all = predicted_samples.percentile(0.975, burnin_samples);
4226 vector<double> mean_all = predicted_samples.mean(burnin_samples);
4227 vector<double> median_all = predicted_samples.percentile(0.5, burnin_samples);
4228
4229 for (size_t i = 0; i < predicted_element_distributions->size(); i++)
4230 {
4231 Range interval;
4232 interval.Set(_range::low, percentile_2_5_all[i]);
4233 interval.Set(_range::high, percentile_97_5_all[i]);
4234 interval.SetMean(mean_all[i]);
4235 interval.SetMedian(median_all[i]);
4236
4237 (*predicted_element_intervals)[predicted_element_distributions->getSeriesName(i)] = interval;
4238 (*predicted_element_intervals)[predicted_element_distributions->getSeriesName(i)].SetValue(
4239 corrected_data.observation(i)->Value());
4240 }
4241
4242 ResultItem predicted_element_intervals_result;
4243 predicted_element_intervals_result.SetName("Predicted Samples Credible Intervals");
4244 predicted_element_intervals_result.SetShowAsString(true);
4245 predicted_element_intervals_result.SetShowTable(true);
4246 predicted_element_intervals_result.SetType(result_type::rangeset_with_observed);
4247 predicted_element_intervals_result.SetResult(predicted_element_intervals);
4248 predicted_element_intervals_result.SetYAxisMode(yaxis_mode::log);
4249 results.Append(predicted_element_intervals_result);
4250
4251 // ========== Result 5: Predicted Isotope Distributions ==========
4252 CMBTimeSeriesSet predicted_isotopes;
4253
4254 for (size_t i = 0; i < predicted_samples.size(); i++)
4255 {
4256 if (corrected_data.GetElementInformation(predicted_samples.getSeriesName(i))->Role ==
4258 {
4259 predicted_isotopes.append(predicted_samples[i], predicted_samples.getSeriesName(i));
4260 }
4261 }
4262
4263 CMBTimeSeriesSet* predicted_isotope_distributions = new CMBTimeSeriesSet();
4264 *predicted_isotope_distributions = predicted_isotopes.distribution(100, burnin_samples);
4265
4266 // Add observed values
4267 for (size_t i = 0; i < predicted_isotopes.size(); i++)
4268 {
4269 predicted_isotope_distributions->SetObservedValue(
4270 i, corrected_data.observation(i + element_names.size())->Value());
4271 }
4272
4273 ResultItem predicted_isotopes_result;
4274 predicted_isotopes_result.SetName("Posterior Predicted Isotopes");
4275 predicted_isotopes_result.SetShowAsString(false);
4276 predicted_isotopes_result.SetShowTable(true);
4277 predicted_isotopes_result.SetType(result_type::distribution_with_observed);
4278 predicted_isotopes_result.SetResult(predicted_isotope_distributions);
4279 results.Append(predicted_isotopes_result);
4280
4281 // ========== Result 6: Isotope Credible Intervals ==========
4282 RangeSet* predicted_isotope_intervals = new RangeSet();
4283 size_t element_offset = corrected_data.ElementOrder().size();
4284
4285 for (size_t i = 0; i < predicted_isotope_distributions->size(); i++)
4286 {
4287 Range interval;
4288 interval.Set(_range::low, percentile_2_5_all[i + element_offset]);
4289 interval.Set(_range::high, percentile_97_5_all[i + element_offset]);
4290 interval.SetMean(mean_all[i + element_offset]);
4291 interval.SetMedian(median_all[i + element_offset]);
4292
4293 (*predicted_isotope_intervals)[predicted_isotope_distributions->getSeriesName(i)] = interval;
4294 (*predicted_isotope_intervals)[predicted_isotope_distributions->getSeriesName(i)].SetValue(
4295 corrected_data.observation(i + element_offset)->Value());
4296 }
4297
4298 ResultItem predicted_isotope_intervals_result;
4299 predicted_isotope_intervals_result.SetName("Predicted Samples Credible Intervals for Isotopes");
4300 predicted_isotope_intervals_result.SetShowAsString(true);
4301 predicted_isotope_intervals_result.SetShowTable(true);
4302 predicted_isotope_intervals_result.SetType(result_type::rangeset_with_observed);
4303 predicted_isotope_intervals_result.SetResult(predicted_isotope_intervals);
4304 predicted_isotope_intervals_result.SetYAxisMode(yaxis_mode::normal);
4305 results.Append(predicted_isotope_intervals_result);
4306
4307 // Set progress to 100%
4308 progress_window->SetProgress(1.0);
4309
4310 return results;
4311}
4312
4313
4315 map<string, string> arguments,
4317 ProgressWindow* progress_window,
4318 const string& working_folder)
4319{
4320 // Initialize with first target sample
4322
4323 // Initialize contribution matrix: 4 columns per source (low, high, median, mean)
4324 const size_t num_target_samples = at(target_group_).size();
4325 const size_t num_columns = numberofsourcesamplesets_ * 4;
4326 CMBMatrix contribution_statistics(num_columns, num_target_samples);
4327
4328 // Set column labels
4329 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
4330 {
4331 const string& source_name = samplesetsorder_[source_idx];
4332 contribution_statistics.SetColumnLabel(source_idx * 4, source_name + "-low");
4333 contribution_statistics.SetColumnLabel(source_idx * 4 + 1, source_name + "-high");
4334 contribution_statistics.SetColumnLabel(source_idx * 4 + 2, source_name + "-median");
4335 contribution_statistics.SetColumnLabel(source_idx * 4 + 3, source_name + "-mean");
4336 }
4337
4338 // Process each target sample
4339 size_t sample_counter = 0;
4340 for (map<string, Elemental_Profile>::iterator sample = at(target_group_).begin();
4341 sample != at(target_group_).end();
4342 sample++)
4343 {
4344 const string& sample_name = sample->first;
4345
4346 // Create output directory for this sample
4347 QDir sample_dir(QString::fromStdString(working_folder) + "/" + QString::fromStdString(sample_name));
4348 if (!sample_dir.exists())
4349 {
4350 sample_dir.mkpath(".");
4351 }
4352
4353 // Set matrix row label
4354 contribution_statistics.SetRowLabel(sample_counter, sample_name);
4355
4356 // Update progress label
4357 progress_window->SetLabel(QString::fromStdString(sample_name));
4358
4359 // Run MCMC analysis for this sample
4360 Results mcmc_results = MCMC(sample_name, arguments, mcmc, progress_window, working_folder);
4361
4362 // Save all result items to text files
4363 for (map<string, ResultItem>::iterator result_item = mcmc_results.begin();
4364 result_item != mcmc_results.end();
4365 result_item++)
4366 {
4367 QString file_path = sample_dir.absolutePath() + "/" +
4368 QString::fromStdString(result_item->first) + ".txt";
4369 QFile output_file(file_path);
4370 output_file.open(QIODevice::WriteOnly | QIODevice::Text);
4371 result_item->second.Result()->writetofile(&output_file);
4372 output_file.close();
4373 }
4374
4375 // Extract contribution statistics from credible intervals
4376 RangeSet* credible_intervals = static_cast<RangeSet*>(
4377 mcmc_results.at("3:Source Contribution Credible Intervals").Result());
4378
4379 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
4380 {
4381 const string contribution_name = samplesetsorder_[source_idx] + "_contribution";
4382 const Range& contribution_interval = credible_intervals->at(contribution_name);
4383
4384 // Store statistics: low (2.5%), high (97.5%), median, mean
4385 contribution_statistics[sample_counter][4 * source_idx] = contribution_interval.Get(_range::low);
4386 contribution_statistics[sample_counter][4 * source_idx + 1] = contribution_interval.Get(_range::high);
4387 contribution_statistics[sample_counter][4 * source_idx + 2] = contribution_interval.Median();
4388 contribution_statistics[sample_counter][4 * source_idx + 3] = contribution_interval.Mean();
4389 }
4390
4391 sample_counter++;
4392
4393 // Update batch progress
4394 progress_window->SetProgress2(double(sample_counter) / double(num_target_samples));
4395
4396 // Clear MCMC progress charts for next sample
4397 progress_window->ClearGraph(0);
4398 progress_window->ClearGraph(1);
4399 progress_window->ClearGraph(2);
4400 }
4401
4402 // Set batch progress to 100%
4403 progress_window->SetProgress2(1.0);
4404
4405 return contribution_statistics;
4406}
4407bool SourceSinkData::ToolsUsed(const string& tool_name)
4408{
4409 // Search for tool in the tools_used list
4410 std::list<string>::iterator iter = std::find(tools_used_.begin(), tools_used_.end(), tool_name);
4411
4412 // Return true if found, false if not found
4413 return (iter != tools_used_.end());
4414}
4415
4417{
4418 // Search for first organic carbon constituent
4419 for (map<string, element_information>::iterator element = element_information_.begin();
4420 element != element_information_.end();
4421 element++)
4422 {
4423 if (element->second.Role == element_information::role::organic_carbon)
4424 {
4425 return element->first;
4426 }
4427 }
4428
4429 // No organic carbon constituent found
4430 return "";
4431}
4432
4434{
4435 // Search for first particle size constituent
4436 for (map<string, element_information>::iterator element = element_information_.begin();
4437 element != element_information_.end();
4438 element++)
4439 {
4440 if (element->second.Role == element_information::role::particle_size)
4441 {
4442 return element->first;
4443 }
4444 }
4445
4446 // No particle size constituent found
4447 return "";
4448}
4449
4451{
4452 vector<string> element_names = GetElementNames();
4453 CMatrix within_group_covariance(element_names.size());
4454
4455 // Accumulate weighted covariance matrices from each source group
4456 int total_degrees_of_freedom = 0;
4457 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4458 source_group != end();
4459 source_group++)
4460 {
4461 if (source_group->first != target_group_)
4462 {
4463 // Weight by degrees of freedom (n - 1) for this group
4464 int group_df = source_group->second.size() - 1;
4465 within_group_covariance += group_df * source_group->second.CalculateCovarianceMatrix();
4466 total_degrees_of_freedom += group_df;
4467 }
4468 }
4469
4470 // Return pooled within-group covariance
4471 return within_group_covariance / double(total_degrees_of_freedom);
4472}
4473
4475{
4476 vector<string> element_names = GetElementNames();
4477 CMatrix between_group_covariance(element_names.size());
4478
4479 // Get overall mean across all sources
4480 CMBVector overall_mean = MeanElementalContent();
4481
4482 // Accumulate weighted outer products of group mean deviations
4483 double total_samples = 0.0;
4484 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4485 source_group != end();
4486 source_group++)
4487 {
4488 if (source_group->first != target_group_)
4489 {
4490 // Compute deviation of group mean from overall mean
4491 CMBVector group_mean = MeanElementalContent(source_group->first);
4492 CMBVector deviation = overall_mean - group_mean;
4493
4494 // Add weighted outer product: n_i × (μ_i - μ)(μ_i - μ)ᵀ
4495 size_t group_size = source_group->second.size();
4496 for (size_t i = 0; i < element_names.size(); i++)
4497 {
4498 for (size_t j = 0; j < element_names.size(); j++)
4499 {
4500 between_group_covariance[i][j] += deviation[i] * deviation[j] * group_size;
4501 }
4502 }
4503
4504 total_samples += group_size;
4505 }
4506 }
4507
4508 // Return normalized between-group covariance
4509 return between_group_covariance / total_samples;
4510}
4511
4513{
4514 vector<string> element_names = GetElementNames();
4515 CMatrix total_scatter(element_names.size());
4516
4517 // Get overall mean across all sources
4518 CMBVector overall_mean = MeanElementalContent();
4519
4520 // Accumulate sum of squared deviations from overall mean
4521 double total_samples = 0.0;
4522 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4523 source_group != end();
4524 source_group++)
4525 {
4526 if (source_group->first != target_group_)
4527 {
4528 // Process each sample in this group
4529 for (map<string, Elemental_Profile>::iterator sample = source_group->second.begin();
4530 sample != source_group->second.end();
4531 sample++)
4532 {
4533 // Add outer product of deviation: (x - μ)(x - μ)ᵀ
4534 for (size_t i = 0; i < element_names.size(); i++)
4535 {
4536 for (size_t j = 0; j < element_names.size(); j++)
4537 {
4538 double deviation_i = overall_mean[i] - sample->second.at(element_names[i]);
4539 double deviation_j = overall_mean[j] - sample->second.at(element_names[j]);
4540 total_scatter[i][j] += deviation_i * deviation_j;
4541 }
4542 }
4543 }
4544
4545 total_samples += source_group->second.size();
4546 }
4547 }
4548
4549 // Return normalized total scatter matrix
4550 return total_scatter / total_samples;
4551}
4552
4554{
4555 // Get covariance matrices
4556 CMatrix_arma within_group_cov = WithinGroupCovarianceMatrix();
4557 CMatrix_arma between_group_cov = BetweenGroupCovarianceMatrix();
4558 CMatrix_arma total_cov = within_group_cov + between_group_cov;
4559
4560 // Compute Wilks' Lambda = |S_W| / |S_T|
4561 double numerator = within_group_cov.det();
4562 double denominator = total_cov.det();
4563
4564 // Use absolute values for numerical stability
4565 return fabs(numerator) / fabs(denominator);
4566}
4567
4569{
4570 int num_elements = GetElementNames().size();
4571
4572 // Get Wilks' Lambda (capped at 1.0)
4573 double wilks_lambda = min(WilksLambda(), 1.0);
4574
4575 // Transform to chi-squared statistic
4576 double sample_size = TotalNumberofSourceSamples();
4577 double correction_factor = (num_elements + (numberofsourcesamplesets_ - 1.0)) / 2.0;
4578 double chi_squared = -(sample_size - 1.0 - correction_factor) * log(wilks_lambda);
4579
4580 // Compute degrees of freedom
4581 double degrees_of_freedom;
4582 if (target_group_ != "")
4583 {
4584 // Target group present: exclude from group count
4585 degrees_of_freedom = num_elements * (numberofsourcesamplesets_ - 2.0);
4586 }
4587 else
4588 {
4589 // No target group
4590 degrees_of_freedom = num_elements * (numberofsourcesamplesets_ - 1.0);
4591 }
4592
4593 // Compute p-value from chi-squared distribution
4594 double p_value = gsl_cdf_chisq_Q(chi_squared, degrees_of_freedom);
4595
4596 return p_value;
4597}
4598
4600{
4601 // Get discriminant eigenvector
4602 CMBVector discriminant_function = DFA_eigvector();
4603
4604 CMBVectorSet projected_scores;
4605
4606 // Project each group onto discriminant axis
4607 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4608 source_group != end();
4609 source_group++)
4610 {
4611 // Compute discriminant scores for this group
4612 CMBVector group_scores = source_group->second.CalculateDotProduct(discriminant_function);
4613 projected_scores.Append(source_group->first, group_scores);
4614 }
4615
4616 return projected_scores;
4617}
4618
4619CMBVectorSet SourceSinkData::DFA_Projected(const string& source1, const string& source2)
4620{
4621 // Get discriminant eigenvector
4622 CMBVector discriminant_function = DFA_eigvector();
4623
4624 CMBVectorSet projected_scores;
4625
4626 // Project each group onto discriminant axis
4627 // Note: Parameters source1 and source2 not currently used
4628 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4629 source_group != end();
4630 source_group++)
4631 {
4632 // Compute discriminant scores for this group
4633 CMBVector group_scores = source_group->second.CalculateDotProduct(discriminant_function);
4634 projected_scores.Append(source_group->first, group_scores);
4635 }
4636
4637 return projected_scores;
4638}
4639
4641{
4642 // Get discriminant eigenvector from current (reduced) dataset
4643 CMBVector discriminant_function = DFA_eigvector();
4644
4645 CMBVectorSet projected_scores;
4646
4647 // Project samples from original dataset onto discriminant axis
4648 for (map<string, Elemental_Profile_Set>::iterator source_group = original->begin();
4649 source_group != original->end();
4650 source_group++)
4651 {
4652 // Skip target group
4653 if (source_group->first != original->target_group_)
4654 {
4655 // Compute discriminant scores for this group
4656 CMBVector group_scores = source_group->second.CalculateDotProduct(discriminant_function);
4657 projected_scores.Append(source_group->first, group_scores);
4658 }
4659 }
4660
4661 return projected_scores;
4662}
4663
4665{
4666 // Get covariance matrices
4667 CMatrix_arma between_group_cov = BetweenGroupCovarianceMatrix();
4668 CMatrix_arma within_group_cov = WithinGroupCovarianceMatrix();
4669
4670 // Invert within-group covariance
4671 CMatrix_arma inv_within_cov = inv(within_group_cov);
4672
4673 // Check if inversion failed (singular matrix)
4674 if (inv_within_cov.getnumrows() == 0)
4675 {
4676 return CMBVector(); // Return empty vector
4677 }
4678
4679 // Compute product matrix for eigenvalue problem
4680 CMatrix_arma product_matrix = inv_within_cov * between_group_cov;
4681
4682 // Solve eigenvalue problem
4683 arma::cx_vec eigenvalues;
4684 arma::cx_mat eigenvectors;
4685 eig_gen(eigenvalues, eigenvectors, product_matrix);
4686
4687 // Extract real parts
4688 CVector_arma real_eigenvalues = GetReal(eigenvalues);
4689 CMatrix_arma real_eigenvectors = GetReal(eigenvectors);
4690
4691 // Find eigenvector corresponding to largest eigenvalue (by absolute value)
4692 size_t max_eigenvalue_index = real_eigenvalues.abs_max_elems();
4693 CMBVector discriminant_function = CVector_arma(real_eigenvectors.getcol(max_eigenvalue_index));
4694
4695 // Label with element names
4696 vector<string> element_names = GetElementNames();
4697 discriminant_function.SetLabels(element_names);
4698
4699 return discriminant_function;
4700}
4701
4702CMBVector SourceSinkData::DFA_weight_vector(const string& source1, const string& source2)
4703{
4704 // Get covariance matrices for both groups
4705 CMatrix_arma cov_matrix1 = at(source1).CalculateCovarianceMatrix();
4706 CMatrix_arma cov_matrix2 = at(source2).CalculateCovarianceMatrix();
4707
4708 // Get mean vectors for both groups
4709 CVector mean_vector1 = at(source1).CalculateElementMeans();
4710 CVector mean_vector2 = at(source2).CalculateElementMeans();
4711 CVector_arma mean1 = mean_vector1;
4712 CVector_arma mean2 = mean_vector2;
4713
4714 // Compute Fisher's linear discriminant: w = (μ₂ - μ₁) / (Σ₁ + Σ₂)
4715 CMatrix_arma pooled_cov = cov_matrix1 + cov_matrix2;
4716 CVector weight_vector_arma = (mean2 - mean1) / pooled_cov;
4717
4718 // Convert to CMBVector and normalize
4719 CMBVector weight_vector = weight_vector_arma;
4720 weight_vector = weight_vector / weight_vector.norm2();
4721
4722 // Label with element names
4723 weight_vector.SetLabels(GetElementNames());
4724
4725 return weight_vector;
4726}
4727
4729{
4730 // Check if group exists
4731 if (count(group_name) == 0)
4732 {
4733 return CMBVector(); // Return empty vector
4734 }
4735
4736 // Compute mean concentrations for this group
4737 vector<string> element_names = GetElementNames();
4738 CMBVector group_mean = at(group_name).CalculateElementMeans();
4739 group_mean.SetLabels(element_names);
4740
4741 return group_mean;
4742}
4743
4745{
4746 vector<string> element_names = GetElementNames();
4747 CMBVector weighted_mean(element_names.size());
4748
4749 // Accumulate weighted means from each source group
4750 double total_samples = 0.0;
4751 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4752 source_group != end();
4753 source_group++)
4754 {
4755 // Skip target group
4756 if (source_group->first != target_group_)
4757 {
4758 // Weight group mean by sample size
4759 size_t group_size = source_group->second.size();
4760 CMBVector group_mean = MeanElementalContent(source_group->first);
4761
4762 weighted_mean += double(group_size) * group_mean;
4763 total_samples += double(group_size);
4764 }
4765 }
4766
4767 // Set labels and normalize by total sample count
4768 weighted_mean.SetLabels(element_order_);
4769
4770 return weighted_mean / total_samples;
4771}
4772
4774 const string& source1,
4775 const string& source2)
4776{
4777 // Initialize result vectors: [0]=p-values, [1]=Wilks' Lambda, [2]=F-test p-values
4778 vector<CMBVector> stepwise_results(3);
4779
4780 vector<string> element_names = GetElementNames();
4781 vector<string> selected_elements;
4782
4783 // Forward selection: add one element at a time
4784 for (size_t step = 0; step < element_names.size(); step++)
4785 {
4786 // Update progress
4787 if (rtw_)
4788 {
4789 rtw_->SetProgress(double(step + 1) / double(element_names.size()));
4790 }
4791
4792 // Find best element to add next
4793 double best_p_value = 100.0;
4794 string best_element;
4795 double best_wilks_lambda;
4796 double best_f_test_p_value;
4797
4798 // Test each unselected element
4799 for (size_t j = 0; j < element_names.size(); j++)
4800 {
4801 // Skip if already selected
4802 if (lookup(selected_elements, element_names[j]) == -1)
4803 {
4804 // Create candidate selection with this element added
4805 vector<string> candidate_elements = selected_elements;
4806 candidate_elements.push_back(element_names[j]);
4807
4808 // Extract dataset with only candidate elements
4809 SourceSinkData candidate_dataset = ExtractSpecificElements(candidate_elements);
4810
4811 // Perform DFA on candidate dataset
4812 DFA_result candidate_dfa = candidate_dataset.DiscriminantFunctionAnalysis(source1, source2);
4813
4814 // Check if DFA succeeded
4815 if (candidate_dfa.eigen_vectors.size() == 0)
4816 {
4817 return stepwise_results; // Return empty results on failure
4818 }
4819
4820 // Update best if this element improves discrimination
4821 if (candidate_dfa.p_values[0] < best_p_value)
4822 {
4823 best_element = element_names[j];
4824 best_p_value = candidate_dfa.p_values[0];
4825 best_wilks_lambda = candidate_dfa.wilkslambda[0];
4826 best_f_test_p_value = candidate_dfa.F_test_P_value[0];
4827 }
4828 }
4829 }
4830
4831 // Record statistics for best element at this step
4832 stepwise_results[0].append(best_element, best_p_value);
4833 stepwise_results[1].append(best_element, best_wilks_lambda);
4834 stepwise_results[2].append(best_element, best_f_test_p_value);
4835
4836 // Add best element to selection
4837 selected_elements.push_back(best_element);
4838 }
4839
4840 return stepwise_results;
4841}
4842
4844{
4845 // Initialize result vectors: [0]=p-values, [1]=Wilks' Lambda, [2]=F-test p-values
4846 vector<CMBVector> stepwise_results(3);
4847
4848 vector<string> element_names = GetElementNames();
4849 vector<string> selected_elements;
4850
4851 // Forward selection: add one element at a time
4852 for (size_t step = 0; step < element_names.size(); step++)
4853 {
4854 // Update progress
4855 if (rtw_)
4856 {
4857 rtw_->SetProgress(double(step + 1) / double(element_names.size()));
4858 }
4859
4860 // Find best element to add next
4861 double best_p_value = 100.0;
4862 string best_element;
4863 double best_wilks_lambda;
4864 double best_f_test_p_value;
4865
4866 // Test each unselected element
4867 for (size_t j = 0; j < element_names.size(); j++)
4868 {
4869 // Skip if already selected
4870 if (lookup(selected_elements, element_names[j]) == -1)
4871 {
4872 // Create candidate selection with this element added
4873 vector<string> candidate_elements = selected_elements;
4874 candidate_elements.push_back(element_names[j]);
4875
4876 // Extract dataset with only candidate elements
4877 SourceSinkData candidate_dataset = ExtractSpecificElements(candidate_elements);
4878
4879 // Compute multi-group DFA statistics
4880 double p_value = candidate_dataset.DFA_P_Value();
4881
4882 // Update best if this element improves discrimination
4883 if (p_value < best_p_value)
4884 {
4885 best_element = element_names[j];
4886 best_p_value = p_value;
4887 best_wilks_lambda = candidate_dataset.WilksLambda();
4888
4889 // Compute F-test p-value from projections
4890 CMBVectorSet projected = candidate_dataset.DFA_Projected();
4891 best_f_test_p_value = projected.FTest_p_value();
4892 }
4893 }
4894 }
4895
4896 // Record statistics for best element at this step
4897 stepwise_results[0].append(best_element, best_p_value);
4898 stepwise_results[1].append(best_element, best_wilks_lambda);
4899 stepwise_results[2].append(best_element, best_f_test_p_value);
4900
4901 // Add best element to selection
4902 selected_elements.push_back(best_element);
4903 }
4904
4905 return stepwise_results;
4906}
4907
4908vector<CMBVector> SourceSinkData::StepwiseDiscriminantFunctionAnalysis(const string& source1)
4909{
4910 // Initialize result vectors: [0]=p-values, [1]=Wilks' Lambda, [2]=F-test p-values
4911 vector<CMBVector> stepwise_results(3);
4912
4913 vector<string> element_names = GetElementNames();
4914 vector<string> selected_elements;
4915
4916 // Forward selection: add one element at a time
4917 for (size_t step = 0; step < element_names.size(); step++)
4918 {
4919 // Update progress
4920 if (rtw_)
4921 {
4922 rtw_->SetProgress(double(step + 1) / double(element_names.size()));
4923 }
4924
4925 // Find best element to add next
4926 double best_p_value = 100.0;
4927 string best_element;
4928 double best_wilks_lambda;
4929 double best_f_test_p_value;
4930
4931 // Test each unselected element
4932 for (size_t j = 0; j < element_names.size(); j++)
4933 {
4934 // Skip if already selected
4935 if (lookup(selected_elements, element_names[j]) == -1)
4936 {
4937 // Create candidate selection with this element added
4938 vector<string> candidate_elements = selected_elements;
4939 candidate_elements.push_back(element_names[j]);
4940
4941 // Extract dataset with only candidate elements
4942 SourceSinkData candidate_dataset = ExtractSpecificElements(candidate_elements);
4943
4944 // Perform one-vs-rest DFA on candidate dataset
4945 DFA_result candidate_dfa = candidate_dataset.DiscriminantFunctionAnalysis(source1);
4946
4947 // Update best if this element improves discrimination
4948 if (candidate_dfa.p_values[0] < best_p_value)
4949 {
4950 best_element = element_names[j];
4951 best_p_value = candidate_dfa.p_values[0];
4952 best_wilks_lambda = candidate_dfa.wilkslambda[0];
4953 best_f_test_p_value = candidate_dfa.F_test_P_value[0];
4954 }
4955 }
4956 }
4957
4958 // Record statistics for best element at this step
4959 stepwise_results[0].append(best_element, best_p_value);
4960 stepwise_results[1].append(best_element, best_wilks_lambda);
4961 stepwise_results[2].append(best_element, best_f_test_p_value);
4962
4963 // Add best element to selection
4964 selected_elements.push_back(best_element);
4965 }
4966
4967 return stepwise_results;
4968}
4969
4970// ========================================
4971// METHOD DEFINITIONS TO MOVE TO sourcesinkdata.cpp
4972// ========================================
4973// These were previously inline in the header and should now be implemented in the .cpp file
4974
4975// ========== Accessors ==========
4976
4977vector<Parameter>& SourceSinkData::Parameters()
4978{
4979 return parameters_;
4980}
4981
4983{
4984 return parameters_.size();
4985}
4986
4988{
4989 return observations_.size();
4990}
4991
4993{
4994 if (i >= 0 && i < parameters_.size())
4995 return &parameters_[i];
4996 else
4997 return nullptr;
4998}
4999
5001{
5002 if (i >= 0 && i < parameters_.size())
5003 return &parameters_[i];
5004 else
5005 return nullptr;
5006}
5007
5009{
5010 if (i >= 0 && i < observations_.size())
5011 return &observations_[i];
5012 else
5013 return nullptr;
5014}
5015
5016bool SourceSinkData::SetTargetGroup(const string& targroup)
5017{
5018 target_group_ = targroup;
5019 return true;
5020}
5021
5023{
5024 return target_group_;
5025}
5026
5031
5036
5038{
5039 rtw_ = _rtw;
5040}
5041
5042map<string, element_information>* SourceSinkData::GetElementInformation()
5043{
5044 return &element_information_;
5045}
5046
5048{
5049 if (element_information_.count(element_name))
5050 return &element_information_.at(element_name);
5051 else
5052 return nullptr;
5053}
5054
5056{
5057 if (element_distributions_.count(element_name))
5058 return &element_distributions_.at(element_name);
5059 else
5060 return nullptr;
5061}
5062
5063ConcentrationSet* SourceSinkData::GetElementDistribution(const string& element_name, const string& sample_group)
5064{
5065 if (!GetSampleSet(sample_group))
5066 {
5067 cout << "Sample Group '" + sample_group + "' does not exist!" << std::endl;
5068 return nullptr;
5069 }
5070 if (!GetSampleSet(sample_group)->GetElementDistribution(element_name))
5071 {
5072 cout << "Element '" + element_name + "' does not exist!" << std::endl;
5073 return nullptr;
5074 }
5075 return GetSampleSet(sample_group)->GetElementDistribution(element_name);
5076}
5077
5078// ========== Ordering Accessors ==========
5079
5081{
5082 return samplesetsorder_;
5083}
5084
5086{
5087 return samplesetsorder_;
5088}
5089
5091{
5092 return constituent_order_;
5093}
5094
5096{
5097 return element_order_;
5098}
5099
5101{
5102 return isotope_order_;
5103}
5104
5106{
5107 return size_om_order_;
5108}
5109
5110// ========== OM/Size Constituent Management ==========
5111
5112void SourceSinkData::SetOMandSizeConstituents(const string& _omconstituent, const string& _sizeconsituent)
5113{
5114 omconstituent_ = _omconstituent;
5115 sizeconsituent_ = _sizeconsituent;
5116}
5117
5118void SourceSinkData::SetOMandSizeConstituents(const vector<string>& _omsizeconstituents)
5119{
5120 if (_omsizeconstituents.size() == 0)
5121 return;
5122 else if (_omsizeconstituents.size() == 1)
5123 omconstituent_ = _omsizeconstituents[0];
5124 else if (_omsizeconstituents.size() == 2)
5125 {
5126 omconstituent_ = _omsizeconstituents[0];
5127 sizeconsituent_ = _omsizeconstituents[1];
5128 }
5129}
5130
5132{
5133 vector<string> out;
5134 out.push_back(omconstituent_);
5135 out.push_back(sizeconsituent_);
5136 return out;
5137}
5138
5139// ========== Options Management ==========
5140
5141QMap<QString, double>* SourceSinkData::GetOptions()
5142{
5143 return &options_;
5144}
5145
5146// ========== Output Path Management ==========
5147
5149{
5150 return outputpath_;
5151}
5152
5153bool SourceSinkData::SetOutputPath(const string& output_path)
5154{
5155 outputpath_ = output_path;
5156 return true;
5157}
5158
5159// ========== Selected Target Sample Management ==========
5160
5161bool SourceSinkData::SetSelectedTargetSample(const string& sample_name)
5162{
5163 // Validate that sample exists in target group
5164 if (count(target_group_) == 0)
5165 return false;
5166
5167 if (at(target_group_).count(sample_name) == 0)
5168 return false;
5169
5170 selected_target_sample_ = sample_name;
5171 return true;
5172}
5173
Matrix class with labeled rows and columns for Chemical Mass Balance analysis.
Definition cmbmatrix.h:19
void SetRowLabel(int i, const string &label)
Sets row label at specified index.
Definition cmbmatrix.h:118
void SetColumnLabel(int i, const string &label)
Sets column label at specified index.
Definition cmbmatrix.h:111
Collection of time series with labels and observed values.
void SetLabel(unsigned int i, const string &label)
Sets custom label for time point.
void SetObservedValue(int i, const double &value)
Sets observed value for comparison at specified index.
void Append(const string &columnlabel, const CMBVectorSet &vectorset)
Adds or replaces a vector set in the collection.
Collection of named CMBVector objects for multi-variable analysis.
void Append(const string &columnlabel, const CMBVector &vectorset)
Adds or replaces a vector in the set.
double FTest_p_value() const
Computes F-test p-value for ANOVA.
Vector class with string labels for Chemical Mass Balance analysis.
Definition cmbvector.h:17
void SetLabels(const vector< string > &label)
Sets all element labels at once.
Definition cmbvector.h:151
void SetLabel(int i, const string &label)
Sets label at specified index.
Definition cmbvector.h:145
int size() const
Gets number of elements in vector.
Definition cmbvector.h:221
void append(const string &label, const double &val)
Appends a labeled element to the vector.
double valueAt(int i) const
Gets value at specified index.
Markov Chain Monte Carlo sampler for Bayesian parameter estimation.
Definition MCMC.h:326
CMBTimeSeriesSet predicted
Model predictions as time series set.
Definition MCMC.h:512
bool SetProperty(const string &varname, const string &value)
Set MCMC properties from string key-value pairs.
Definition MCMC.hpp:68
bool step(int k, int chain_counter)
Perform single MCMC step for one chain.
Definition MCMC.hpp:337
T * Model
Pointer to the model object being calibrated.
Definition MCMC.h:334
void initialize(CMBTimeSeriesSet *results, bool random=false)
Initialize MCMC chains with starting parameter values.
Definition MCMC.hpp:200
Manages a collection of concentration measurements with statistical analysis.
double CalculateSSE(double mean_value=-999) const
Calculate sum of squared errors from mean.
double CalculateMean() const
Calculate arithmetic mean.
Distribution * GetEstimatedDistribution()
Get estimated distribution (for MCMC optimization)
void SetEstimatedSigma(double value)
double CalculateStdDevLog(double mean_value=-999) const
Calculate standard deviation of log-transformed values.
vector< double > EstimateDistributionParameters(distribution_type dist_type=distribution_type::none)
Estimate distribution parameters from data.
double CalculateMeanLog() const
Calculate mean of log-transformed values.
void SetEstimatedMu(double value)
vector< unsigned int > CalculateRanks() const
Calculate ranks of values (1 = smallest)
double GetMaximum() const
Get maximum value.
void AppendSet(const ConcentrationSet &other)
Append all values from another set.
Distribution * GetFittedDistribution()
Get fitted distribution (from data)
void AppendValue(double value)
Append a single concentration value.
double CalculateSSELog(double mean_value=-999) const
Calculate sum of squared errors from log mean.
double CalculateStdDev(double mean_value=-999) const
Calculate standard deviation.
Source contribution container for Chemical Mass Balance results.
Represents a parametric probability distribution for uncertainty quantification.
vector< double > parameters
Distribution parameters vector.
double EvalLog(const double &x)
Evaluate natural logarithm of probability density.
double DataMean()
Get the stored empirical mean.
distribution_type distribution
The type of probability distribution.
double Mean(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Calculate the mean (expected value) of the distribution.
Manages a collection of elemental profiles (samples) for source fingerprinting analysis.
ConcentrationSet * GetElementDistribution(const string &element_name)
Get distribution for a specific element (mutable)
vector< string > GetSampleNames() const
Get all sample names in the set.
Elemental_Profile * AppendProfile(const string &name, const Elemental_Profile &profile=Elemental_Profile(), map< string, element_information > *elementinfo=nullptr)
Add a new sample profile to the set.
Distribution * GetEstimatedDistribution(const string &element_name)
Get estimated distribution for an element (for MCMC optimization)
void UpdateElementDistributions()
Rebuild element distributions from all profiles.
bool SetContributionSoftmax(const double &value)
Set softmax-transformed contribution.
vector< double > GetConcentrationsForSample(const string &sample_name) const
Get all element concentrations for a specific sample.
Elemental_Profile * GetProfile(const string &name)
Get profile by sample name (mutable)
bool ReadFromJsonObject(const QJsonObject &jsonobject) override
Deserialize object from JSON format.
bool SetContribution(const double &value)
Set contribution value for this source.
double GetContribution() const
Get contribution value.
double GetContributionSoftmax() const
Get softmax-transformed contribution.
void AppendProfiles(const Elemental_Profile_Set &profiles, map< string, element_information > *elementinfo)
Add multiple profiles from another set.
Container for elemental concentration data of a single sediment sample.
bool AppendElement(const string &name, double val=0.0)
Add new element to profile.
double GetValue(const string &name) const
Get element concentration by name.
static double GetRndUniF(double xmin, double xmax)
void SetName(const std::string &nam)
Definition observation.h:12
void SetPredictedValue(const double &value)
Definition observation.h:25
void AppendValues(const double &t, const double &val)
double Value()
Definition observation.h:17
double PredictedValue()
Definition observation.h:24
Represents a model parameter with prior distribution and constraints.
Definition parameter.h:73
void SetPriorDistribution(distribution_type dist_type)
Set the type of prior probability distribution.
Definition parameter.h:128
double Value() const
Get the current parameter value.
Definition parameter.h:258
void SetRange(const vector< double > &rng)
Set the parameter range from a vector.
Definition parameter.cpp:18
string Name() const
Get the parameter name.
Definition parameter.h:248
void SetName(const string &nam)
Set the parameter name/identifier.
Definition parameter.h:240
void SetProgress2(const double &prog)
void SetYAxisTitle(const QString &title, int chart=0)
void AppendPoint(const double &x, const double &y, int chart=0)
void SetXRange(const double &x0, const double &x1, int chart=0)
void SetProgress(const double &prog)
void ClearGraph(int chart=0)
void SetTitle(const QString &title, int chart=0)
void SetLabel(const QString &label)
Definition range.h:8
double Mean() const
Definition range.h:20
void Set(_range lowhigh, const double &value)
Definition range.cpp:64
double Median() const
Definition range.h:22
void SetValue(const double value)
Definition range.h:24
double mean
Definition range.h:29
void SetMedian(const double &m)
Definition range.h:23
void SetMean(const double &m)
Definition range.h:21
double Get(_range lowhigh) const
Definition range.cpp:71
void SetShowGraph(bool state)
Definition resultitem.h:80
void setYAxisTitle(const string &title)
Definition resultitem.h:39
void SetXAxisMode(xaxis_mode mode)
Definition resultitem.h:26
void SetYLimit(_range highlow, const double &value)
Definition resultitem.h:66
void SetName(const string &_name)
Definition resultitem.h:21
Interface * Result() const
Definition resultitem.h:17
void SetResult(Interface *_result)
Definition resultitem.h:18
void SetShowTable(bool state)
Definition resultitem.h:78
void setXAxisTitle(const string &title)
Definition resultitem.h:31
void SetYAxisMode(yaxis_mode mode)
Definition resultitem.h:25
void SetShowAsString(bool value)
Definition resultitem.h:29
void SetType(const result_type &_type)
Definition resultitem.h:23
void Append(const ResultItem &)
Definition results.cpp:25
void SetError(const string &_error)
Definition results.h:25
void AppendError(const string &_error)
Definition results.h:26
void SetName(const string &_name)
Definition results.h:21
Elemental_Profile * GetElementalProfile(const string &sample_name)
Finds and retrieves an elemental profile by sample name.
CMatrix BetweenGroupCovarianceMatrix()
Computes between-group covariance matrix.
vector< string > IsotopesToBeUsedInCMB()
Identifies isotopes to be used in CMB analysis.
profiles_data ExtractConcentrationData(const vector< vector< string > > &indicators) const
Extract concentration data for specified samples.
Parameter * GetElementDistributionSigmaParameter(size_t element_index, size_t source_index)
Retrieves pointer to the σ (std dev) parameter for an element distribution.
void AssignAllDistributions()
Assign distributions to all elements at both dataset and group levels.
vector< string > element_order_
string GetOutputPath() const
Get the output directory path.
bool InitializeParametersAndObservations(const string &targetsamplename, estimation_mode est_mode=estimation_mode::elemental_profile_and_contribution)
Initialize parameters and observations for MCMC optimization.
Elemental_Profile Sample(const string &sample_name) const
Retrieves an elemental profile by sample name.
QMap< QString, double > * GetOptions()
Retrieves pointer to the options map.
void SetProgressWindow(ProgressWindow *_rtw)
Sets the progress window for displaying optimization progress.
size_t ParametersCount()
Returns the number of parameters.
bool PerformRegressionVsOMAndSize(const string &om, const string &particle_size, regression_form form, const double &p_value_threshold=0.05)
Performs multiple linear regression of elements vs OM and particle size.
vector< string > size_om_order_
bool ReadFromFile(QFile *fil)
Loads complete dataset from a JSON file.
vector< Parameter > & Parameters()
Retrieves reference to the parameters vector.
ResultItem GetCalculatedElementMu()
Computes μ parameters from fitted log-normal distributions for source elements.
ResultItem GetObservedElementalProfile()
Retrieves the observed elemental concentrations for the selected target sample.
bool ReadElementInformationfromJsonObject(const QJsonObject &jsonobject)
Deserializes element information metadata from a JSON object.
map< string, vector< double > > ExtractElementDataByGroup(const string &element) const
Extract concentration data for a specific element from all groups.
Elemental_Profile_Set DifferentiationPower_Percentage(bool include_target)
Computes rank-based differentiation percentage for all source pairs.
CVector GetContributionVector(bool include_all=true)
Retrieves the current source contribution fractions.
CMBTimeSeriesSet BootStrap(const double &percentage, unsigned int num_iterations, string target_sample, bool use_softmax)
Performs bootstrap uncertainty analysis on source contributions.
double GetElementDistributionMuValue(size_t element_index, size_t source_index)
Retrieves the current value of the μ parameter for an element distribution.
vector< string > GetElementNames() const
Get all element names in the dataset.
SourceSinkData ExtractChemicalElements(bool isotopes) const
Extract only chemical elements (and optionally isotopes)
Observation * observation(size_t i)
Retrieves pointer to an observation by index.
SourceSinkData & operator=(const SourceSinkData &other)
Assignment operator.
Elemental_Profile_Set TheRest(const string &excluded_source)
Collects all source samples except those from a specified source group.
CMBVector ANOVA(bool use_log)
Performs one-way ANOVA for all elements across source groups.
CMBVector OptimalBoxCoxParameters()
Computes optimal Box-Cox transformation parameters for all elements.
CVector PredictTarget_Isotope(parameter_mode param_mode=parameter_mode::direct)
Predicts target sample isotopic compositions based on source contributions.
void OutlierAnalysisForAll(const double &lower_threshold=-3, const double &upper_threshold=3)
Performs outlier detection on all source groups.
bool SolveLevenberg_Marquardt(transformation trans=transformation::linear)
Solves for optimal source contributions using the Levenberg-Marquardt algorithm.
CMatrix_arma ResidualJacobian_arma()
Calculates the Jacobian matrix of residuals with respect to contributions (Armadillo)
CMBVectorSet DFA_Projected()
Projects all groups onto the discriminant function axis.
ConcentrationSet * GetElementDistribution(const string &element_name)
Retrieves pointer to element distribution at dataset level.
SourceSinkData BoxCoxTransformed(bool calculate_optimal_lambda=false)
Applies Box-Cox transformation to all source groups for normalization.
CVector OneStepLevenberg_Marquardt(double lambda)
Performs one iteration of the Levenberg-Marquardt optimization algorithm.
CMatrix ResidualJacobian_softmax()
Calculates the Jacobian using softmax parameterization of contributions.
void SetParameterEstimationMode(estimation_mode est_mode)
Sets the estimation mode for parameter optimization.
CMBVector DFA_weight_vector(const string &source1, const string &source2)
Computes discriminant weight vector for two-group comparison.
Elemental_Profile_Set * GetSampleSet(const string &name)
Get a sample set (source or target group) by name.
Elemental_Profile_Set * AppendSampleSet(const string &name, const Elemental_Profile_Set &elemental_profile_set=Elemental_Profile_Set())
Add a new sample set (source or target group) to the dataset.
void Clear()
Clear all data from the object.
double regression_p_value_threshold_
CMBMatrix MCMC_Batch(map< string, string > arguments, CMCMC< SourceSinkData > *mcmc, ProgressWindow *progress_window, const string &working_folder)
Performs batch MCMC analysis on all target samples.
CMatrix WithinGroupCovarianceMatrix()
Computes pooled within-group covariance matrix.
double LogLikelihoodModelvsMeasured(estimation_mode est_mode=estimation_mode::elemental_profile_and_contribution)
Calculates the log-likelihood of the model prediction versus measured data.
CVector ObservedDataforSelectedSample_Isotope_delta(const string &SelectedTargetSample="")
Retrieves the observed isotopic data in delta notation.
Elemental_Profile t_TestPValue(const string &source1, const string &source2, bool use_log)
Computes t-test p-values for element-wise differences between two sources.
vector< Observation > observations_
Distribution * GetFittedDistribution(const string &element_name)
Get the fitted distribution for a specific element at dataset level.
double WilksLambda()
Computes Wilks' Lambda statistic for multivariate group separation.
map< string, ConcentrationSet > ExtractConcentrationSet()
Extracts concentration distributions for all elements across sources.
list< string > tools_used_
bool SetParameterValue(size_t index, double value)
Sets a parameter value and updates corresponding model components.
CVector GetParameterValue() const
Retrieves all current parameter values as a vector.
vector< string > SourceGroupNames() const
Retrieves the names of all source groups (excluding target)
bool ToolsUsed(const string &tool_name)
Checks if a specific analysis tool has been used.
vector< ResultItem > GetMLRResults()
Retrieves multiple linear regression results for all sample groups.
vector< string > RandomlypickSamples(const double &percentage) const
Randomly selects a subset of source samples.
CVector ObservedDataforSelectedSample_Isotope(const string &SelectedTargetSample="")
Retrieves the observed isotopic data for a selected target sample.
estimation_mode parameter_estimation_mode_
bool SetOutputPath(const string &output_path)
Set the output directory path.
ResultItem GetPredictedElementalProfile_Isotope(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Generates predicted isotope delta values for the target sample.
CMBVector BracketTest(const string &target_sample, bool correct_based_on_om_n_size)
Performs bracket test to check if target concentrations fall within source ranges.
bool ReadElementDatafromJsonObject(const QJsonObject &jsonobject)
Deserializes elemental profile data from a JSON object.
Elemental_Profile_Set ExtractSamplesAsProfileSet(const vector< vector< string > > &indicators) const
Extract samples as an Elemental_Profile_Set.
vector< string > isotope_order_
CVector GetContributionVectorSoftmax()
Retrieves the softmax parameters for source contributions.
void IncludeExcludeElementsBasedOn(const vector< string > &elements)
Sets element inclusion based on a specified list.
ResultItem GetPredictedElementalProfile(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Generates predicted elemental concentrations for the target sample.
CVector GetPredictedValues()
Retrieves predicted values for all observations.
QJsonObject ElementInformationToJsonObject() const
Exports element information metadata to a JSON object.
ResultItem GetCalculatedElementSigma()
Computes estimated standard deviations for all source elements.
QJsonObject OptionsToJsonObject() const
Exports analysis options/settings to a JSON object.
void SetOMandSizeConstituents(const string &_omconstituent, const string &_sizeconsituent)
Sets the names of OM and particle size constituents.
Parameter * parameter(size_t i)
Retrieves pointer to a parameter by index.
bool ReadToolsUsedFromJsonObject(const QJsonArray &jsonarray)
Deserializes the list of analysis tools from a JSON array.
SourceSinkData RandomlyEliminateSourceSamples(const double &percentage)
Creates dataset with randomly excluded source samples for validation.
CVector Gradient(const CVector &parameters, estimation_mode est_mode)
Computes the normalized gradient of the log-likelihood function.
void SetContributionSoftmax(size_t source_index, double softmax_value)
Sets a single softmax parameter value.
vector< string > constituent_order_
SourceSinkData ReplaceSourceAsTarget(const string &source_sample_name) const
Creates a new dataset with a source sample designated as the target.
double LogPriorContributions()
Calculates the log prior probability for source contributions.
CVector ResidualVector()
Calculates the combined residual vector for elemental and isotopic predictions.
SourceSinkData CreateCorrectedAndFilteredDataset(bool exclude_samples, bool exclude_elements, bool omnsizecorrect, const string &target="") const
Create a corrected and filtered copy of the dataset.
QJsonObject ElementDataToJsonObject() const
Exports all elemental profile data to a JSON object.
vector< string > NegativeValueCheck()
Checks for zero or negative concentration values across all sources.
CMBTimeSeriesSet VerifySource(const string &source_group, bool use_softmax, bool apply_om_size_correction)
Performs leave-one-out validation on a source group.
bool SetSelectedTargetSample(const string &sample_name)
Sets the currently selected target sample for analysis.
map< string, element_information > * GetElementInformation()
Retrieves pointer to the element information map.
void AddtoToolsUsed(const string &tool)
Adds a tool name to the list of tools used in analysis.
QMap< QString, double > options_
DFA_result DiscriminantFunctionAnalysis()
Performs discriminant function analysis for all source groups.
vector< string > ElementsToBeUsedInCMB()
Identifies chemical elements to be used in CMB analysis.
map< string, ConcentrationSet > element_distributions_
vector< string > SizeOMOrder()
Retrieves the ordering of size and OM constituents.
void PopulateConstituentOrders()
Populates all element ordering vectors used throughout CMB analysis.
vector< ResultItem > GetSourceProfiles()
Retrieves elemental profiles for all source groups.
CMatrix BuildSourceMeanMatrix_Isotopes(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Builds the source mean concentration matrix for isotopes.
map< string, element_information > element_information_
void PopulateElementInformation(const map< string, element_information > *ElementInfo=nullptr)
Populate element information metadata.
ProgressWindow * rtw_
ResultItem GetCalculatedElementMeans()
Computes estimated mean concentrations for all source elements.
Results MCMC(const string &target_sample, map< string, string > arguments, CMCMC< SourceSinkData > *mcmc, ProgressWindow *progress_window, const string &working_folder)
Performs Markov Chain Monte Carlo analysis for Bayesian source apportionment.
Elemental_Profile_Set DifferentiationPower(bool use_log, bool include_target)
Computes differentiation power for all source pairs.
string GetParameterName(int index) const
Get the name of a parameter by its index.
double LogLikelihoodSourceElementalDistributions()
Calculates the log-likelihood of source elemental distributions.
vector< string > samplesetsorder_
bool InitializeContributionsRandomly()
Initialize source contributions randomly (linear constraint)
double LogLikelihood(estimation_mode est_mode=estimation_mode::elemental_profile_and_contribution)
Calculates the total log-likelihood for Bayesian source apportionment.
QString Role(const element_information::role &role) const
Converts element role enum to string representation.
size_t ObservationsCount()
Returns the number of observations.
CVector GradientUpdate(estimation_mode estmode=estimation_mode::elemental_profile_and_contribution)
Performs one gradient ascent step with adaptive step size.
SourceSinkData CreateCorrectedDataset(const string &target, bool omnsizecorrect, map< string, element_information > *elementinfo)
Create a corrected copy of the dataset for a specific target sample.
vector< CMBVector > StepwiseDiscriminantFunctionAnalysis()
Performs stepwise discriminant analysis across all source groups.
vector< string > GetSampleNames(const string &group_name) const
Get all sample names within a specific group.
CMBVector MeanElementalContent()
Computes weighted mean elemental concentrations across all sources.
CMatrix TotalScatterMatrix()
Computes total scatter matrix.
ResultItem GetEstimatedElementSigma()
Retrieves estimated σ parameters from Bayesian inference for source elements.
double GrandMean(const string &element, bool use_log)
Computes grand mean concentration for an element across all sources.
void IncludeExcludeAllElements(bool include_in_analysis)
Sets inclusion flag for all elements.
string FirstOMConstituent()
Retrieves the name of the first organic matter constituent.
CVector OneStepLevenberg_Marquardt_softmax(double lambda)
Performs one iteration of Levenberg-Marquardt using softmax parameterization.
CVector PredictTarget(parameter_mode param_mode=parameter_mode::direct)
Predicts target sample elemental concentrations based on source contributions.
vector< string > ElementOrder()
Retrieves the ordering of chemical elements.
vector< string > GetGroupNames() const
Get all group names in the dataset.
string SelectedTargetSample() const
Retrieves the name of the currently selected target sample.
ResultItem GetObservedvsModeledElementalProfile(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Creates a comparison of observed vs modeled elemental profiles.
vector< string > IsotopeOrder()
Retrieves the ordering of isotopes.
int CountElements(bool exclude_elements) const
Count the number of elements in the dataset.
SourceSinkData ExtractSpecificElements(const vector< string > &element_list) const
Extract specific elements from source groups.
bool InitializeContributionsRandomlySoftmax()
Initialize source contributions randomly (softmax transformation)
int TotalNumberofSourceSamples() const
Counts the total number of source samples across all source groups.
Parameter * GetElementDistributionMuParameter(size_t element_index, size_t source_index)
Retrieves pointer to the μ (mean) parameter for an element distribution.
estimation_mode ParameterEstimationMode()
Retrieves the current estimation mode.
element_data ExtractElementConcentrations(const string &element, const string &group) const
Extract concentration data for a specific element from a group.
double DFA_P_Value()
Computes p-value for discriminant function analysis.
ResultItem GetEstimatedElementMean()
Computes actual mean concentrations from estimated log-normal parameters.
string GetTargetGroup() const
Retrieves the name of the target group.
void PopulateElementDistributions()
Populate element distributions from all groups.
Elemental_Profile_Set DifferentiationPower_P_value(bool include_target)
Computes t-test p-values for all source pairs.
CVector PredictTarget_Isotope_delta(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Predicts target sample isotopic compositions in delta notation.
ResultItem GetContribution()
Packages source contributions into a ResultItem for output.
CVector_arma ResidualVector_arma()
Calculates the combined residual vector using Armadillo vector format.
vector< string > AllSourceSampleNames() const
Retrieves names of all samples across all source groups.
string FirstSizeConstituent()
Retrieves the name of the first particle size constituent.
bool ReadOptionsfromJsonObject(const QJsonObject &jsonobject)
Deserializes analysis options from a JSON object.
string selected_target_sample_
double GetObjectiveFunctionValue()
Returns the objective function value for optimization algorithms.
CMBVector DFATransformed(const CMBVector &eigenvector, const string &source_group)
Projects samples onto a discriminant function axis.
CMBTimeSeriesSet LM_Batch(transformation transform, bool apply_om_size_correction, map< string, vector< string > > &negative_elements)
Solves CMB model for all target samples using Levenberg-Marquardt.
vector< Parameter > parameters_
bool SetTargetGroup(const string &targroup)
Sets the target/sink group designation.
vector< string > GetSourceOrder() const
Retrieves the ordering of source groups.
vector< string > ConstituentOrder()
Retrieves the ordering of all constituents.
CVector ObservedDataforSelectedSample(const string &SelectedTargetSample="")
Retrieves the observed elemental data for a selected target sample.
ResultItem GetObservedElementalProfile_Isotope()
Retrieves the observed isotope delta values for the selected target sample.
CMatrix ResidualJacobian()
Calculates the Jacobian matrix of residuals with respect to contributions.
CMatrix BuildSourceMeanMatrix(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Builds the source mean concentration matrix for chemical elements.
CVector GetSourceContributions()
Retrieves the contributions from all sources.
double GetElementDistributionSigmaValue(size_t element_index, size_t source_index)
Retrieves the current value of the σ parameter for an element distribution.
QJsonArray ToolsUsedToJsonObject() const
Exports the list of analysis tools used to a JSON array.
double error_stdev_isotope_
Elemental_Profile_Set LumpAllProfileSets()
Combines all source samples into a single profile set.
double LogLikelihoodModelvsMeasured_Isotope(estimation_mode est_mode=estimation_mode::elemental_profile_and_contribution)
Calculates the log-likelihood of model versus measured isotopic data.
bool WriteDataToFile(QFile *file)
Writes elemental profile data to a text file.
bool WriteToFile(QFile *file)
Writes dataset to a text file.
vector< string > SamplesetsOrder()
Retrieves the ordering of sample sets.
void SetContribution(size_t source_index, double contribution_value)
Sets a single source contribution value.
ResultItem GetEstimatedElementMu()
Retrieves estimated μ parameters from Bayesian inference for source elements.
CMBVector DFA_eigvector()
Computes the primary discriminant function eigenvector.
ResultItem GetObservedvsModeledElementalProfile_Isotope(parameter_mode param_mode=parameter_mode::based_on_fitted_distribution)
Creates a comparison of observed vs modeled isotope delta values.
vector< string > OMandSizeConstituents()
Retrieves the names of OM and particle size constituents.
SourceSinkData()
Default constructor.
parameter_mode
Specifies whether to use direct data statistics or fitted distribution parameters.
@ based_on_fitted_distribution
Calculate from fitted distribution parameters.
@ direct
Use empirical statistics from data.
@ dirichlet
Dirichlet distribution (treated as uniform on simplex in current implementation)
@ lognormal
Lognormal distribution: ln(x) ~ N(μ, σ²), for strictly positive variables.
@ normal
Normal (Gaussian) distribution: p(x) = N(μ, σ²)
@ low
Lower bound of the parameter range.
@ high
Upper bound of the parameter range.
@ distribution_with_observed
@ predicted_concentration
@ rangeset_with_observed
@ elemental_profile_set
transformation
estimation_mode
@ elemental_profile_and_contribution
@ source_elemental_profiles_based_on_source_data
CMBVectorSet projected
CMBVector p_values
CMBVectorSet eigen_vectors
CMBVector F_test_P_value
CMBVector wilkslambda
CMBVectorSetSet multi_projected
vector< string > sample_names
vector< double > values
Metadata describing an element's role and properties in analysis.
double standard_ratio
Standard isotopic ratio for normalization.
enum element_information::role Role
role
Classification of element types.
@ element
Standard chemical element concentration.
@ isotope
Isotopic ratio (e.g., 206Pb/207Pb)
@ organic_carbon
Organic carbon content.
@ particle_size
Particle size distribution parameter.
@ do_not_include
Exclude from all analyses.
bool include_in_analysis
Whether to include in statistical analyses.
string base_element
Parent element for isotopes.
vector< vector< double > > values
vector< string > element_names
vector< string > sample_names