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
676namespace {
677
692bool FittedParametersAvailable(const Distribution* fitted,
693 size_t needed,
694 const string& element_name,
695 const string& group_name)
696{
697 if (fitted != nullptr && fitted->parameters.size() >= needed)
698 return true;
699
700 std::cerr << "InitializeParametersAndObservations: no fitted distribution for '"
701 << element_name << "' in group '" << group_name
702 << "'. Build the distributions with PopulateElementDistributions() "
703 "and AssignAllDistributions() before initializing." << std::endl;
704 return false;
705}
706
707} // namespace
708
710 const string& targetsamplename,
711 estimation_mode est_mode)
712{
713 // Populate element ordering vectors
715 selected_target_sample_ = targetsamplename;
716
717 // Validate data is loaded
718 if (empty())
719 {
720 std::cerr << "Data has not been loaded!" << std::endl;
721 return false;
722 }
723
724 numberofsourcesamplesets_ = size() - 1;
725
726 // Clear existing parameters and observations
727 parameters_.clear();
728 observations_.clear();
729 samplesetsorder_.clear();
730
731 // ========== Setup Source Contribution Parameters ==========
732
734 {
735 // Create contribution parameters for all sources except last (calculated from constraint)
736 for (auto& [group_name, profile_set] : *this)
737 {
738 if (group_name != GetTargetGroup())
739 {
740 Parameter p;
741 p.SetName(group_name + "_contribution");
743 p.SetRange(0, 1);
744 parameters_.push_back(p);
745 samplesetsorder_.push_back(group_name);
746 }
747 }
748 // Remove last contribution parameter (calculated from sum constraint)
749 parameters_.pop_back();
750 }
751 else
752 {
753 // Build source order without creating parameters
754 for (auto& [group_name, profile_set] : *this)
755 {
756 if (group_name != GetTargetGroup())
757 {
758 samplesetsorder_.push_back(group_name);
759 }
760 }
761 }
762
763 // ========== Count Elements and Isotopes ==========
764
767
768 for (const auto& [element_name, elem_info] : element_information_)
769 {
770 if (elem_info.include_in_analysis)
771 {
772 if (elem_info.Role == element_information::role::element) {
774 }
775 else if (elem_info.Role == element_information::role::isotope) {
777 }
778 }
779 }
780
781 // ========== Initialize Estimated Distributions from Fitted ==========
782
783 // For elements
784 for (const auto& [element_name, elem_info] : element_information_)
785 {
786 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
787 {
788 for (auto& [group_name, profile_set] : *this)
789 {
790 if (group_name != target_group_)
791 {
792 // Copy fitted distribution to estimated (starting point for optimization)
793 *profile_set.GetEstimatedDistribution(element_name) =
794 *profile_set.GetFittedDistribution(element_name);
795 }
796 }
797 }
798 }
799
800 // For isotopes
801 for (const auto& [element_name, elem_info] : element_information_)
802 {
803 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
804 {
805 for (auto& [group_name, profile_set] : *this)
806 {
807 if (group_name != target_group_)
808 {
809 *profile_set.GetEstimatedDistribution(element_name) =
810 *profile_set.GetFittedDistribution(element_name);
811 }
812 }
813 }
814 }
815
816 // ========== Setup Source Profile Parameters (if estimating profiles) ==========
817
819 {
820 // Mu parameters for elements (lognormal)
821 for (const auto& [element_name, elem_info] : element_information_)
822 {
823 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
824 {
825 for (auto& [group_name, profile_set] : *this)
826 {
827 if (group_name != target_group_)
828 {
829 Parameter p;
830 p.SetName(group_name + "_" + element_name + "_mu");
832
833 // A dataset that has not had its distributions built
834 // has no fitted parameters to centre the range on.
835 // Reading them anyway is an out-of-range access on an
836 // empty vector, so report the omission instead.
837 Distribution* estimated = profile_set.GetEstimatedDistribution(element_name);
838 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
839
840 if (estimated == nullptr ||
841 !FittedParametersAvailable(fitted, 1, element_name, group_name))
842 {
843 return false;
844 }
845
846 // Set estimated distribution type
848
849 // Set parameter range around fitted mu
850 p.SetRange(fitted->parameters[0] - 0.2, fitted->parameters[0] + 0.2);
851
852 parameters_.push_back(p);
853 }
854 }
855 }
856 }
857
858 // Mu parameters for isotopes (normal)
859 for (const auto& [element_name, elem_info] : element_information_)
860 {
861 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
862 {
863 for (auto& [group_name, profile_set] : *this)
864 {
865 if (group_name != target_group_)
866 {
867 Parameter p;
868 p.SetName(group_name + "_" + element_name + "_mu");
870
871 Distribution* estimated = profile_set.GetEstimatedDistribution(element_name);
872 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
873
874 if (estimated == nullptr ||
875 !FittedParametersAvailable(fitted, 1, element_name, group_name))
876 {
877 return false;
878 }
879
880 // Set estimated distribution type
882
883 // Set parameter range around fitted mu
884 p.SetRange(fitted->parameters[0] - 0.2, fitted->parameters[0] + 0.2);
885
886 parameters_.push_back(p);
887 }
888 }
889 }
890 }
891
892 // Sigma parameters for elements
893 for (const auto& [element_name, elem_info] : element_information_)
894 {
895 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
896 {
897 for (auto& [group_name, profile_set] : *this)
898 {
899 if (group_name != target_group_)
900 {
901 Parameter p;
902 p.SetName(group_name + "_" + element_name + "_sigma");
904
905 // Set parameter range around fitted sigma (with bounds)
906 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
907
908 if (!FittedParametersAvailable(fitted, 2, element_name, group_name))
909 return false;
910
911 double lower = std::max(fitted->parameters[1] * 0.8, 0.001);
912 double upper = std::max(fitted->parameters[1] / 0.8, 2.0);
913 p.SetRange(lower, upper);
914
915 parameters_.push_back(p);
916 }
917 }
918 }
919 }
920
921 // Sigma parameters for isotopes
922 for (const auto& [element_name, elem_info] : element_information_)
923 {
924 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
925 {
926 for (auto& [group_name, profile_set] : *this)
927 {
928 if (group_name != target_group_)
929 {
930 Parameter p;
931 p.SetName(group_name + "_" + element_name + "_sigma");
933
934 // Set parameter range around fitted sigma (with bounds)
935 const Distribution* fitted = profile_set.GetFittedDistribution(element_name);
936
937 if (!FittedParametersAvailable(fitted, 2, element_name, group_name))
938 return false;
939
940 double lower = std::max(fitted->parameters[1] * 0.8, 0.001);
941 double upper = std::max(fitted->parameters[1] / 0.8, 2.0);
942 p.SetRange(lower, upper);
943
944 parameters_.push_back(p);
945 }
946 }
947 }
948 }
949 }
950
951 // ========== Setup Error and Observation Parameters ==========
952
954 {
955 // Error standard deviation for elements
956 Parameter error_param;
957 error_param.SetName("Error STDev");
959 error_param.SetRange(0.01, 0.1);
960 parameters_.push_back(error_param);
961
962 // Create observations for elements
963 for (const auto& [element_name, elem_info] : element_information_)
964 {
965 if (elem_info.Role == element_information::role::element && elem_info.include_in_analysis)
966 {
967 Observation obs;
968 obs.SetName(targetsamplename + "_" + element_name);
969 obs.AppendValues(0, GetElementalProfile(targetsamplename)->GetValue(element_name));
970 observations_.push_back(obs);
971 }
972 }
973
974 // Error standard deviation for isotopes
975 Parameter error_param_isotope;
976 error_param_isotope.SetName("Error STDev for isotopes");
978 error_param_isotope.SetRange(0.01, 0.1);
979 parameters_.push_back(error_param_isotope);
980
981 // Create observations for isotopes
982 for (const auto& [element_name, elem_info] : element_information_)
983 {
984 if (elem_info.Role == element_information::role::isotope && elem_info.include_in_analysis)
985 {
986 Observation obs;
987 obs.SetName(targetsamplename + "_" + element_name);
988 obs.AppendValues(0, GetElementalProfile(targetsamplename)->GetValue(element_name));
989 observations_.push_back(obs);
990 }
991 }
992 }
993
994 return true;
995}
996
997
999{
1000 CVector contributions(size()-1);
1001 for (unsigned long int i=0; i<size()-2; i++)
1002 {
1003 contributions[i] = parameter(i)->Value();
1004 }
1005 contributions[size()-2] = 1 - contributions.sum();
1006 return contributions;
1007}
1008
1009
1011{
1012 if (GetSourceContributions().min()<0)
1013 return -1e10;
1014 else
1015 return 0;
1016}
1017
1019{
1020 double logLikelihood = 0;
1021 for (unsigned int element_counter=0; element_counter<element_order_.size(); element_counter++)
1022 {
1023 for (unsigned int source_group_counter=0; source_group_counter<numberofsourcesamplesets_; source_group_counter++)
1024 {
1025 Elemental_Profile_Set *this_source_group = GetSampleSet(samplesetsorder_[source_group_counter]);
1026
1027
1028 for (map<string,Elemental_Profile>::iterator sample = this_source_group->begin(); sample!=this_source_group->end(); sample++)
1029 {
1030 logLikelihood += this_source_group->GetElementDistribution(element_order_[element_counter])->GetEstimatedDistribution()->EvalLog(sample->second.GetValue(element_order_[element_counter]));
1031 }
1032
1033 }
1034 }
1035 return logLikelihood;
1036}
1037
1038CVector SourceSinkData::ObservedDataforSelectedSample(const string &SelectedTargetSample)
1039{
1040 CVector observed_data(element_order_.size());
1041 for (unsigned int i=0; i<element_order_.size(); i++)
1042 { if (SelectedTargetSample!="")
1043 observed_data[i] = this->GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(element_order_[i]);
1044 else if (selected_target_sample_!="")
1046 }
1047 return observed_data;
1048}
1049
1050CVector SourceSinkData::ObservedDataforSelectedSample_Isotope(const string &SelectedTargetSample)
1051{
1052 CVector observed_data(isotope_order_.size());
1053 for (unsigned int i=0; i<isotope_order_.size(); i++)
1054 { string corresponding_element = element_information_[isotope_order_[i]].base_element;
1055 if (SelectedTargetSample!="")
1056 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);
1057 else if (selected_target_sample_!="")
1058 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);
1059 }
1060 return observed_data;
1061}
1062
1063CVector SourceSinkData::ObservedDataforSelectedSample_Isotope_delta(const string &SelectedTargetSample)
1064{
1065 CVector observed_data(isotope_order_.size());
1066 for (unsigned int i=0; i<isotope_order_.size(); i++)
1067 { string corresponding_element = element_information_[isotope_order_[i]].base_element;
1068 if (SelectedTargetSample!="")
1069 observed_data[i] = this->GetSampleSet(GetTargetGroup())->GetProfile(SelectedTargetSample)->GetValue(isotope_order_[i]);
1070 else if (selected_target_sample_!="")
1072 }
1073 return observed_data;
1074}
1075
1076
1078{
1079 // Determine parameter mode based on estimation mode
1083
1084 // Get predicted and observed concentrations
1085 CVector predicted_concentrations = PredictTarget(param_mode);
1086 CVector observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_);
1087
1088 // Check validity of predictions (all values must be positive for log-transformation)
1089 if (predicted_concentrations.min() <= 0)
1090 {
1091 return -1e10; // Invalid prediction: return extremely low likelihood
1092 }
1093
1094 // Calculate log-likelihood in log-space
1095 // Formula: log(L) = -n*log(σ) - ||log(C_pred) - log(C_obs)||² / (2σ²)
1096 const double num_elements = predicted_concentrations.num;
1097 const double normalization_term = num_elements * log(error_stdev_);
1098
1099 CVector log_residuals = predicted_concentrations.Log() - observed_concentrations.Log();
1100 const double sum_squared_residuals = pow(log_residuals.norm2(), 2);
1101 const double variance_term = 2.0 * pow(error_stdev_, 2);
1102
1103 double log_likelihood = -normalization_term - (sum_squared_residuals / variance_term);
1104
1105 return log_likelihood;
1106}
1107
1109{
1110 // Determine parameter mode based on estimation mode
1114
1115 // Get predicted and observed isotopic delta values
1116 CVector predicted_deltas = PredictTarget_Isotope_delta(param_mode);
1118
1119 // Calculate log-likelihood in linear space (delta values)
1120 // Formula: log(L) = -n*log(σ_iso) - ||δ_pred - δ_obs||² / (2σ_iso²)
1121 const double num_isotopes = predicted_deltas.num;
1122 const double normalization_term = num_isotopes * log(error_stdev_isotope_);
1123
1124 CVector residuals = predicted_deltas - observed_deltas;
1125 const double sum_squared_residuals = pow(residuals.norm2(), 2);
1126 const double variance_term = 2.0 * pow(error_stdev_isotope_, 2);
1127
1128 double log_likelihood = -normalization_term - (sum_squared_residuals / variance_term);
1129
1130 return log_likelihood;
1131}
1132
1133
1135{
1136 // Get predicted values using direct parameter mode
1137 CVector predicted_concentrations = PredictTarget(parameter_mode::direct);
1138 CVector predicted_deltas = PredictTarget_Isotope_delta(parameter_mode::direct);
1139
1140 // Check for invalid predictions
1141 if (!predicted_concentrations.is_finite())
1142 {
1143 qDebug() << "Warning: Non-finite predicted concentrations detected in ResidualVector()";
1144 }
1145
1146 // Get observed values for the selected target sample
1147 CVector observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_);
1149
1150 // Calculate residuals
1151 // Elemental: log-space residuals (log(predicted) - log(observed))
1152 CVector elemental_residuals = predicted_concentrations.Log() - observed_concentrations.Log();
1153
1154 // Isotopic: linear residuals (predicted_delta - observed_delta)
1155 CVector isotopic_residuals = predicted_deltas - observed_deltas;
1156
1157 // Combine residuals: [elemental_residuals, isotopic_residuals]
1158 CVector combined_residuals = elemental_residuals;
1159 combined_residuals.append(isotopic_residuals);
1160
1161 // Check for non-finite residuals (indicates numerical issues)
1162 //if (!combined_residuals.is_finite())
1163 //{
1164 // qDebug() << "Warning: Non-finite residuals detected in ResidualVector()";
1165 // qDebug() << "Contribution vector:" << GetContributionVector().toString();
1166 // qDebug() << "Contribution vector (softmax):" << GetContributionVectorSoftmax().toString();
1167 //}
1168
1169 return combined_residuals;
1170}
1171
1173{
1174 // Get predicted values (returns CVector with .vec member for Armadillo compatibility)
1175 CVector_arma predicted_concentrations = PredictTarget().vec;
1176 CVector_arma predicted_deltas = PredictTarget_Isotope_delta().vec;
1177
1178 // Get observed values for the selected target sample
1179 CVector_arma observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_).vec;
1180 CVector_arma observed_deltas = ObservedDataforSelectedSample_Isotope_delta(selected_target_sample_).vec;
1181
1182 // Calculate residuals
1183 // Elemental: log-space residuals (log(predicted) - log(observed))
1184 CVector_arma elemental_residuals = predicted_concentrations.Log() - observed_concentrations.Log();
1185
1186 // Isotopic: linear residuals (predicted_delta - observed_delta)
1187 CVector_arma isotopic_residuals = predicted_deltas - observed_deltas;
1188
1189 // Combine residuals: [elemental_residuals, isotopic_residuals]
1190 CVector_arma combined_residuals = elemental_residuals;
1191 combined_residuals.append(isotopic_residuals);
1192
1193 return combined_residuals;
1194}
1195
1196
1198{
1199 const size_t num_sources = GetSourceOrder().size();
1200 const size_t num_parameters = num_sources - 1; // Last contribution is implicit
1201 const size_t num_residuals = element_order_.size() + isotope_order_.size();
1202
1203 CMatrix_arma jacobian(num_parameters, num_residuals);
1204
1205 // Store base state
1206 CVector_arma base_contributions = GetContributionVector(false).vec;
1207 CVector_arma base_residuals = ResidualVector_arma();
1208
1209 // Compute derivatives using finite differences
1210 for (unsigned int i = 0; i < num_parameters; i++)
1211 {
1212 // Adaptive epsilon: smaller when far from 0.5, larger when near boundaries
1213 const double epsilon = (0.5 - base_contributions[i]) * 1e-6;
1214
1215 // Perturb contribution
1216 SetContribution(i, base_contributions[i] + epsilon);
1217 CVector_arma perturbed_residuals = ResidualVector_arma();
1218
1219 // Compute derivative: ∂residuals/∂contribution_i
1220 jacobian.setcol(i, (perturbed_residuals - base_residuals) / epsilon);
1221
1222 // Restore original contribution
1223 SetContribution(i, base_contributions[i]);
1224 }
1225
1226 return jacobian;
1227}
1228
1230{
1231 const size_t num_sources = GetSourceOrder().size();
1232 const size_t num_parameters = num_sources - 1; // Last contribution is implicit
1233 const size_t num_residuals = element_order_.size() + isotope_order_.size();
1234
1235 CMatrix jacobian(num_parameters, num_residuals);
1236
1237 // Store base state
1238 CVector base_contributions = GetContributionVector(false);
1239 CVector base_residuals = ResidualVector();
1240
1241 // Compute derivatives using finite differences
1242 for (unsigned int i = 0; i < num_parameters; i++)
1243 {
1244 // Adaptive epsilon: smaller when far from 0.5, larger when near boundaries
1245 const double epsilon = (0.5 - base_contributions[i]) * 1e-3;
1246
1247 // Perturb contribution
1248 SetContribution(i, base_contributions[i] + epsilon);
1249 CVector perturbed_residuals = ResidualVector();
1250
1251 // Compute derivative: ∂residuals/∂contribution_i
1252 jacobian.setrow(i, (perturbed_residuals - base_residuals) / epsilon);
1253
1254 // Restore original contribution
1255 SetContribution(i, base_contributions[i]);
1256 }
1257
1258 return jacobian;
1259}
1260
1262{
1263 const size_t num_sources = GetSourceOrder().size();
1264 const size_t num_residuals = element_order_.size() + isotope_order_.size();
1265
1266 CMatrix jacobian(num_sources, num_residuals); // All sources are parameters in softmax
1267
1268 // Store base state
1269 CVector base_softmax_params = GetContributionVectorSoftmax();
1270 CVector base_residuals = ResidualVector();
1271
1272 // Compute derivatives using finite differences
1273 for (unsigned int i = 0; i < num_sources; i++)
1274 {
1275 // Sign-dependent epsilon for better numerical behavior
1276 const double epsilon = -sign(base_softmax_params[i]) * 1e-3;
1277
1278 // Perturb softmax parameter
1279 CVector perturbed_params = base_softmax_params;
1280 perturbed_params[i] += epsilon;
1281 SetContributionSoftmax(perturbed_params);
1282
1283 CVector perturbed_residuals = ResidualVector();
1284
1285 // Compute derivative: ∂residuals/∂softmax_param_i
1286 jacobian.setrow(i, (perturbed_residuals - base_residuals) / epsilon);
1287
1288 // Restore original softmax parameters
1289 SetContributionSoftmax(base_softmax_params);
1290 }
1291
1292 return jacobian;
1293}
1294
1296{
1297 // Get current residuals and Jacobian
1298 CVector residuals = ResidualVector();
1299 CMatrix jacobian = ResidualJacobian();
1300
1301 // Compute J^T J (normal equations matrix)
1302 CMatrix jacobian_transpose_jacobian = jacobian * Transpose(jacobian);
1303
1304 // Apply Levenberg-Marquardt damping: (1 + λ) on diagonal
1305 jacobian_transpose_jacobian.ScaleDiagonal(1.0 + lambda);
1306
1307 // Compute right-hand side: J^T r
1308 CVector jacobian_times_residuals = jacobian * residuals;
1309
1310 // Check for near-singular matrix and add regularization if needed
1311 if (det(jacobian_transpose_jacobian) <= 1e-6)
1312 {
1313 // Add additional regularization: λI
1314 const size_t matrix_size = jacobian_transpose_jacobian.getnumcols();
1315 CMatrix identity = CMatrix::Diag(matrix_size);
1316 jacobian_transpose_jacobian += lambda * identity;
1317 }
1318
1319 // Solve for parameter update: dx = (J^T J)^(-1) J^T r
1320 CVector parameter_update = jacobian_times_residuals / jacobian_transpose_jacobian;
1321
1322 return parameter_update;
1323}
1324
1326{
1327 // Get current residuals and Jacobian (softmax parameterization)
1328 CVector residuals = ResidualVector();
1329 CMatrix jacobian = ResidualJacobian_softmax();
1330
1331 // Compute J^T J (normal equations matrix)
1332 CMatrix jacobian_transpose_jacobian = jacobian * Transpose(jacobian);
1333
1334 // Apply Levenberg-Marquardt damping: (1 + λ) on diagonal
1335 jacobian_transpose_jacobian.ScaleDiagonal(1.0 + lambda);
1336
1337 // Compute right-hand side: J^T r
1338 CVector jacobian_times_residuals = jacobian * residuals;
1339
1340 // Check for near-singular matrix and add regularization if needed
1341 if (det(jacobian_transpose_jacobian) <= 1e-6)
1342 {
1343 // Add additional regularization: λI
1344 const size_t matrix_size = jacobian_transpose_jacobian.getnumcols();
1345 CMatrix identity = CMatrix::Diag(matrix_size);
1346 jacobian_transpose_jacobian += lambda * identity;
1347 }
1348
1349 // Solve for parameter update: dx = (J^T J)^(-1) J^T r
1350 CVector parameter_update = jacobian_times_residuals / jacobian_transpose_jacobian;
1351
1352 return parameter_update;
1353}
1354
1356{
1357 // Initialize contributions based on parameterization
1358 if (trans == transformation::linear)
1360 else if (trans == transformation::softmax)
1362
1363 // Algorithm parameters
1364 const double tolerance = 1e-10;
1365 const int max_iterations = 1000;
1366 const double improvement_threshold = 0.8; // Error must reduce to <80% for lambda decrease
1367 const double lambda_decrease_factor = 1.2;
1368 const double lambda_increase_factor = 1.2;
1369 const double lambda_no_update_factor = 5.0;
1370
1371 // Initialize tracking variables
1372 double lambda = 1.0;
1373 double current_error = ResidualVector().norm2();
1374 double previous_error = 1000.0;
1375 double initial_param_change = 10000.0;
1376 double current_param_change = 10000.0;
1377 int iteration = 0;
1378
1379 // Main optimization loop
1380 while (current_error > tolerance &&
1381 current_param_change > tolerance &&
1382 iteration < max_iterations)
1383 {
1384 // Store current parameter values
1385 CVector current_params;
1386 if (trans == transformation::linear)
1387 current_params = GetContributionVector(false);
1388 else if (trans == transformation::softmax)
1389 current_params = GetContributionVectorSoftmax();
1390
1391 previous_error = current_error;
1392
1393 // Compute parameter update step
1394 CVector parameter_update;
1395 if (trans == transformation::linear)
1396 parameter_update = OneStepLevenberg_Marquardt(lambda);
1397 else if (trans == transformation::softmax)
1398 parameter_update = OneStepLevenberg_Marquardt_softmax(lambda);
1399
1400 // Handle singular Jacobian (no valid update computed)
1401 if (parameter_update.num == 0)
1402 {
1403 lambda *= lambda_no_update_factor;
1404 continue; // Skip to next iteration
1405 }
1406
1407 // Track parameter change magnitude
1408 current_param_change = parameter_update.norm2();
1409 if (iteration == 0)
1410 initial_param_change = current_param_change;
1411
1412 // Apply parameter update
1413 CVector updated_params = current_params - parameter_update;
1414 if (trans == transformation::linear)
1415 SetContribution(updated_params);
1416 else if (trans == transformation::softmax)
1417 SetContributionSoftmax(updated_params);
1418
1419 // Evaluate new error
1420 CVector residuals = ResidualVector();
1421 current_error = residuals.norm2();
1422
1423 // Adaptive lambda adjustment based on error change
1424 if (current_error < previous_error * improvement_threshold)
1425 {
1426 // Good progress: decrease lambda (move toward Gauss-Newton)
1427 lambda /= lambda_decrease_factor;
1428 }
1429 else if (current_error > previous_error)
1430 {
1431 // Error increased: reject step, increase lambda (move toward gradient descent)
1432 lambda *= lambda_increase_factor;
1433
1434 // Restore previous parameters
1435 if (trans == transformation::linear)
1436 SetContribution(current_params);
1437 else if (trans == transformation::softmax)
1438 SetContributionSoftmax(current_params);
1439
1440 current_error = previous_error;
1441 }
1442 // else: modest improvement, keep lambda unchanged
1443
1444 // Update progress visualization if available
1445 if (rtw_)
1446 {
1447 rtw_->AppendPoint(iteration, current_error);
1448 rtw_->SetXRange(0, iteration);
1449 rtw_->SetProgress(1.0 - current_param_change / initial_param_change);
1450 }
1451
1452 iteration++;
1453 }
1454
1455 // The reported fraction approaches 1 without reaching it, because the loop
1456 // exits once the parameter change falls below the tolerance rather than
1457 // when it reaches zero. Report completion explicitly so the display does
1458 // not stop short of the end.
1459 if (rtw_)
1460 rtw_->SetProgress(1.0);
1461
1462 // TODO: Return convergence status instead of always false
1463 return false;
1464}
1465
1466
1468{
1469 // Compute predicted concentrations: C = Source_Matrix × Contribution_Vector
1470 CMatrix source_mean_matrix = BuildSourceMeanMatrix(param_mode);
1471 CVector contribution_vector = GetContributionVector();
1472 CVector predicted_concentrations = source_mean_matrix * contribution_vector;
1473
1474 // Update stored predicted values in observation objects for each element
1475 for (unsigned int i = 0; i < element_order_.size(); i++)
1476 {
1477 observation(i)->SetPredictedValue(predicted_concentrations[i]);
1478 }
1479
1480 return predicted_concentrations;
1481}
1482
1484{
1485 // Compute predicted isotopic concentrations: C = Source_Matrix × Contribution_Vector
1486 CMatrix source_isotope_matrix = BuildSourceMeanMatrix_Isotopes(param_mode);
1487 CVector contribution_vector = GetContributionVector();
1488 CVector predicted_isotope_concentrations = source_isotope_matrix * contribution_vector;
1489
1490 return predicted_isotope_concentrations;
1491}
1492
1494{
1495 CMatrix SourceMeanMat = BuildSourceMeanMatrix(param_mode);
1496 CMatrix SourceMeanMat_Iso = BuildSourceMeanMatrix_Isotopes(param_mode);
1497 CVector C_elements = SourceMeanMat*GetContributionVector();
1498 CVector C = SourceMeanMat_Iso*GetContributionVector();
1499 for (unsigned int i=0; i<numberofisotopes_; i++)
1500 {
1501 string corresponding_element = element_information_[isotope_order_[i]].base_element;
1502 double predicted_corresponding_element_concentration = C_elements[lookup(element_order_,corresponding_element)];
1503 double ratio = C[i]/predicted_corresponding_element_concentration;
1504 double standard_ratio = element_information_[isotope_order_[i]].standard_ratio;
1505 C[i] = (ratio/standard_ratio-1.0)*1000.0;
1506 }
1507 for (unsigned int i=element_order_.size(); i<element_order_.size()+isotope_order_.size(); i++)
1508 {
1510 }
1511 return C;
1512}
1513
1514
1519
1521{
1522 // Initialize likelihood components
1523 double source_data_log_likelihood = 0.0;
1524 double element_observation_log_likelihood = 0.0;
1525 double isotope_observation_log_likelihood = 0.0;
1526
1527 // Component 1: Log-likelihood of source data given estimated distributions
1528 // P(Y | μ, σ) - Only included when estimating source profiles
1529 if (est_mode != estimation_mode::only_contributions) {
1530 source_data_log_likelihood = LogLikelihoodSourceElementalDistributions();
1531 }
1532
1533 // Component 2: Log-prior on contributions
1534 // P(f) - Always included (uniform Dirichlet prior)
1535 double contribution_log_prior = LogPriorContributions();
1536
1537 // Component 3: Log-likelihood of observations given model predictions
1538 // P(C_obs | f, μ, σ) - Only when fitting to target sample
1540 // Elements: log P(C_obs | C_pred)
1541 element_observation_log_likelihood = LogLikelihoodModelvsMeasured(est_mode);
1542
1543 // Isotopes: log P(δ_obs | δ_pred)
1544 isotope_observation_log_likelihood = LogLikelihoodModelvsMeasured_Isotope(est_mode);
1545 }
1546
1547 // Sum all components to get total log-likelihood
1548 double total_log_likelihood = source_data_log_likelihood +
1549 element_observation_log_likelihood +
1550 isotope_observation_log_likelihood +
1551 contribution_log_prior;
1552
1553 return total_log_likelihood;
1554}
1555
1557{
1558 const size_t num_elements = element_order_.size();
1559 const size_t num_sources = numberofsourcesamplesets_;
1560
1561 // Initialize source mean matrix: rows = elements, cols = sources
1562 // Matrix entry [i,j] = mean concentration of element i in source j
1563 CMatrix source_means(num_elements, num_sources);
1564
1565 // Fill matrix with mean concentrations for each element and source
1566 for (size_t element_idx = 0; element_idx < num_elements; element_idx++)
1567 {
1568 const string& element_name = element_order_[element_idx];
1569
1570 for (size_t source_idx = 0; source_idx < num_sources; source_idx++)
1571 {
1572 const string& source_name = samplesetsorder_[source_idx];
1573 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1574
1575 // Get estimated distribution for this element in this source
1576 Distribution* element_dist = source_group->GetEstimatedDistribution(element_name);
1577
1578 // Calculate mean based on parameter mode
1580 // Parametric mean from distribution parameters (μ, σ)
1581 // For lognormal: Mean = exp(μ + σ²/2)
1582 source_means[element_idx][source_idx] = element_dist->Mean();
1583 }
1584 else {
1585 // Empirical mean calculated directly from data points
1586 source_means[element_idx][source_idx] = element_dist->DataMean();
1587 }
1588 }
1589 }
1590
1591 return source_means;
1592}
1593
1595{
1596 const size_t num_isotopes = isotope_order_.size();
1597 const size_t num_sources = numberofsourcesamplesets_;
1598
1599 // Initialize source isotope matrix: rows = isotopes, cols = sources
1600 // Matrix entry [i,j] = mean absolute concentration of isotope i in source j
1601 CMatrix source_isotope_means(num_isotopes, num_sources);
1602
1603 // Fill matrix with mean isotope concentrations (converted from delta)
1604 for (size_t isotope_idx = 0; isotope_idx < num_isotopes; isotope_idx++)
1605 {
1606 const string& isotope_name = isotope_order_[isotope_idx];
1607 const element_information& isotope_info = element_information_[isotope_name];
1608 const string& base_element = isotope_info.base_element;
1609 const double standard_ratio = isotope_info.standard_ratio;
1610
1611 for (size_t source_idx = 0; source_idx < num_sources; source_idx++)
1612 {
1613 const string& source_name = samplesetsorder_[source_idx];
1614 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1615
1616 // Get distributions for isotope and its base element
1617 Distribution* isotope_dist = source_group->GetEstimatedDistribution(isotope_name);
1618 Distribution* base_element_dist = source_group->GetEstimatedDistribution(base_element);
1619
1620 // Retrieve mean values based on parameter mode
1621 double mean_delta;
1622 double mean_base_concentration;
1623
1625 // Use parametric means from fitted distributions
1626 mean_delta = isotope_dist->Mean();
1627 mean_base_concentration = base_element_dist->Mean();
1628 }
1629 else {
1630 // Use empirical means from actual data
1631 mean_delta = isotope_dist->DataMean();
1632 mean_base_concentration = base_element_dist->DataMean();
1633 }
1634
1635 // Convert delta notation to absolute concentration
1636 // Formula: [isotope] = (δ/1000 + 1) × R_standard × [base_element]
1637 double delta_ratio = (mean_delta / 1000.0) + 1.0;
1638 double absolute_isotope_concentration = delta_ratio * standard_ratio * mean_base_concentration;
1639
1640 source_isotope_means[isotope_idx][source_idx] = absolute_isotope_concentration;
1641 }
1642 }
1643
1644 return source_isotope_means;
1645}
1646
1648{
1649 // Determine vector size based on whether to include constrained contribution
1650 const size_t num_contributions = include_all ?
1653
1654 CVector contributions(num_contributions);
1655
1656 // Retrieve contribution values from each source group
1657 for (size_t source_idx = 0; source_idx < num_contributions; source_idx++)
1658 {
1659 const string& source_name = samplesetsorder_[source_idx];
1660 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1661 contributions[source_idx] = source_group->GetContribution();
1662 }
1663
1664 return contributions;
1665}
1666
1668{
1669 const size_t num_sources = numberofsourcesamplesets_;
1670 CVector softmax_parameters(num_sources);
1671
1672 // Retrieve softmax parameter values from each source group
1673 for (size_t source_idx = 0; source_idx < num_sources; source_idx++)
1674 {
1675 const string& source_name = samplesetsorder_[source_idx];
1676 Elemental_Profile_Set* source_group = GetSampleSet(source_name);
1677 softmax_parameters[source_idx] = source_group->GetContributionSoftmax();
1678 }
1679
1680 return softmax_parameters;
1681}
1682
1683
1684
1685
1686void SourceSinkData::SetContribution(size_t source_index, double contribution_value)
1687{
1688 // Set the specified contribution
1689 const string& source_name = samplesetsorder_[source_index];
1690 GetSampleSet(source_name)->SetContribution(contribution_value);
1691
1692 // Update last contribution to maintain sum constraint
1693 const size_t last_source_index = samplesetsorder_.size() - 1;
1694 const string& last_source_name = samplesetsorder_[last_source_index];
1695
1696 // Calculate constrained contribution: c_last = 1 - Σ(c_i)
1697 double sum_of_independent = GetContributionVector(false).sum();
1698 double constrained_contribution = 1.0 - sum_of_independent;
1699
1700 GetSampleSet(last_source_name)->SetContribution(constrained_contribution);
1701}
1702
1703void SourceSinkData::SetContributionSoftmax(size_t source_index, double softmax_value)
1704{
1705 // Set softmax parameter (no constraint to maintain)
1706 const string& source_name = samplesetsorder_[source_index];
1707 GetSampleSet(source_name)->SetContributionSoftmax(softmax_value);
1708}
1709
1710void SourceSinkData::SetContribution(const CVector& contributions)
1711{
1712 // Set each contribution (last contribution updated automatically each time)
1713 for (size_t i = 0; i < static_cast<size_t>(contributions.num); i++)
1714 {
1715 SetContribution(i, contributions.vec[i]);
1716 }
1717
1718 // Validate: all contributions should be non-negative
1719 if (contributions.min() < 0.0) {
1720 std::cerr << "Warning: Negative contribution detected in SetContribution()" << std::endl;
1721 std::cerr << " Min value: " << contributions.min() << std::endl;
1722 }
1723}
1724
1725void SourceSinkData::SetContributionSoftmax(const CVector& softmax_params)
1726{
1727 // Compute softmax normalization denominator: Σ exp(x_i)
1728 double denominator = 0.0;
1729 for (size_t i = 0; i < static_cast<size_t>(softmax_params.num); i++)
1730 {
1731 denominator += exp(softmax_params[i]);
1732 }
1733
1734 // Apply softmax transformation: c_i = exp(x_i) / Σ exp(x_j)
1735 for (size_t i = 0; i < static_cast<size_t>(softmax_params.num); i++)
1736 {
1737 double softmax_param = softmax_params.at(i);
1738 double contribution = exp(softmax_param) / denominator;
1739
1740 // Set both softmax parameter and computed contribution
1741 SetContributionSoftmax(i, softmax_param);
1743 }
1744
1745 // Validate: resulting contributions should be valid
1746 CVector resulting_contributions = GetContributionVector();
1747 if (resulting_contributions.min() < 0.0) {
1748 std::cerr << "Warning: Invalid contributions after softmax transformation" << std::endl;
1749 std::cerr << " Min contribution: " << resulting_contributions.min() << std::endl;
1750 std::cerr << " This should not happen with softmax - check for numerical issues" << std::endl;
1751 }
1752}
1753
1755 size_t element_index,
1756 size_t source_index)
1757{
1758 // Parameter vector layout:
1759 // [0 to n-2]: Contribution parameters (n-1 independent contributions)
1760 // [n-1 onwards]: Element μ parameters
1761
1762 const size_t num_contribution_params = size() - 1; // Exclude target group
1763 const size_t element_mu_base_index = num_contribution_params;
1764
1765 // Calculate index for this element's μ in this source
1766 // Elements are stored in blocks: all sources for element 0, then all sources for element 1, etc.
1767 const size_t parameter_index = element_mu_base_index +
1768 (element_index * numberofsourcesamplesets_) +
1769 source_index;
1770
1771 // Bounds check
1772 if (parameter_index >= parameters_.size()) {
1773 std::cerr << "Error: Parameter index out of bounds in GetElementDistributionMuParameter" << std::endl;
1774 return nullptr;
1775 }
1776
1777 return &parameters_[parameter_index];
1778}
1779
1781 size_t element_index,
1782 size_t source_index)
1783{
1784 // Parameter vector layout:
1785 // [0 to n-2]: Contribution parameters
1786 // [n-1 to n-1 + num_elements × num_sources - 1]: Element μ parameters
1787 // [next block]: Element σ parameters ← We're here
1788
1789 const size_t num_contribution_params = size() - 1;
1790 const size_t num_element_mu_params = numberofconstituents_ * numberofsourcesamplesets_;
1791 const size_t element_sigma_base_index = num_contribution_params + num_element_mu_params;
1792
1793 // Calculate index for this element's σ in this source
1794 const size_t parameter_index = element_sigma_base_index +
1795 (element_index * numberofsourcesamplesets_) +
1796 source_index;
1797
1798 // Bounds check
1799 if (parameter_index >= parameters_.size()) {
1800 std::cerr << "Error: Parameter index out of bounds in GetElementDistributionSigmaParameter" << std::endl;
1801 return nullptr;
1802 }
1803
1804 return &parameters_[parameter_index];
1805}
1806
1808 size_t element_index,
1809 size_t source_index)
1810{
1811 Parameter* mu_param = GetElementDistributionMuParameter(element_index, source_index);
1812
1813 if (mu_param == nullptr) {
1814 std::cerr << "Warning: Unable to retrieve μ parameter for element "
1815 << element_index << ", source " << source_index << std::endl;
1816 return 0.0;
1817 }
1818
1819 return mu_param->Value();
1820}
1821
1823 size_t element_index,
1824 size_t source_index)
1825{
1826 Parameter* sigma_param = GetElementDistributionSigmaParameter(element_index, source_index);
1827
1828 if (sigma_param == nullptr) {
1829 std::cerr << "Warning: Unable to retrieve σ parameter for element "
1830 << element_index << ", source " << source_index << std::endl;
1831 return 0.0;
1832 }
1833
1834 return sigma_param->Value();
1835}
1836
1837bool SourceSinkData::SetParameterValue(size_t index, double value)
1838{
1839 // Validate index
1840 if (index >= parameters_.size()) {
1841 return false;
1842 }
1843
1844 // Determine which parameter blocks are active based on estimation mode
1845 const bool estimating_contributions =
1847 const bool estimating_profiles =
1849
1850 // Calculate parameter block boundaries
1851 const size_t num_contribution_params = estimating_contributions ? (numberofsourcesamplesets_ - 1) : 0;
1852 const size_t num_element_mu_params = numberofconstituents_ * numberofsourcesamplesets_;
1853 const size_t num_isotope_mu_params = numberofisotopes_ * numberofsourcesamplesets_;
1854 const size_t num_element_sigma_params = num_element_mu_params;
1855 const size_t num_isotope_sigma_params = num_isotope_mu_params;
1856
1857 // Update parameter value
1858 parameters_[index].SetValue(value);
1859
1860 // ========== Block 1: Contribution Parameters ==========
1861 if (estimating_contributions && index < num_contribution_params)
1862 {
1863 // Update this source's contribution
1865
1866 // Update last contribution to maintain sum constraint: c_n = 1 - Σ(c_1...c_{n-1})
1867 const size_t last_source_index = numberofsourcesamplesets_ - 1;
1868 double sum_of_independent = GetContributionVector(false).sum();
1869 GetSampleSet(samplesetsorder_[last_source_index])->SetContribution(1.0 - sum_of_independent);
1870
1871 return true;
1872 }
1873
1874 // ========== Block 2: Element μ Parameters ==========
1875 size_t element_mu_start = num_contribution_params;
1876 size_t element_mu_end = element_mu_start + num_element_mu_params;
1877
1878 if (estimating_profiles &&
1880 index >= element_mu_start && index < element_mu_end)
1881 {
1882 size_t offset = index - element_mu_start;
1883 size_t element_index = offset / numberofsourcesamplesets_;
1884 size_t source_index = offset % numberofsourcesamplesets_;
1885
1886 GetElementDistribution(element_order_[element_index], samplesetsorder_[source_index])
1887 ->SetEstimatedMu(value);
1888
1889 return true;
1890 }
1891
1892 // ========== Block 3: Isotope μ Parameters ==========
1893 size_t isotope_mu_start = element_mu_end;
1894 size_t isotope_mu_end = isotope_mu_start + num_isotope_mu_params;
1895
1896 if (estimating_profiles &&
1898 index >= isotope_mu_start && index < isotope_mu_end)
1899 {
1900 size_t offset = index - isotope_mu_start;
1901 size_t isotope_index = offset / numberofsourcesamplesets_;
1902 size_t source_index = offset % numberofsourcesamplesets_;
1903
1904 GetElementDistribution(isotope_order_[isotope_index], samplesetsorder_[source_index])
1905 ->SetEstimatedMu(value);
1906
1907 return true;
1908 }
1909
1910 // ========== Block 4: Element σ Parameters ==========
1911 size_t element_sigma_start = isotope_mu_end;
1912 size_t element_sigma_end = element_sigma_start + num_element_sigma_params;
1913
1914 if (estimating_profiles &&
1916 index >= element_sigma_start && index < element_sigma_end)
1917 {
1918 if (value < 0.0) {
1919 std::cerr << "Error: Element σ parameter cannot be negative (value = "
1920 << value << ")" << std::endl;
1921 return false;
1922 }
1923
1924 size_t offset = index - element_sigma_start;
1925 size_t element_index = offset / numberofsourcesamplesets_;
1926 size_t source_index = offset % numberofsourcesamplesets_;
1927
1928 GetElementDistribution(element_order_[element_index], samplesetsorder_[source_index])
1929 ->SetEstimatedSigma(value);
1930
1931 return true;
1932 }
1933
1934 // ========== Block 5: Isotope σ Parameters ==========
1935 size_t isotope_sigma_start = element_sigma_end;
1936 size_t isotope_sigma_end = isotope_sigma_start + num_isotope_sigma_params;
1937
1938 if (estimating_profiles &&
1940 index >= isotope_sigma_start && index < isotope_sigma_end)
1941 {
1942 if (value < 0.0) {
1943 std::cerr << "Error: Isotope σ parameter cannot be negative (value = "
1944 << value << ")" << std::endl;
1945 return false;
1946 }
1947
1948 size_t offset = index - isotope_sigma_start;
1949 size_t isotope_index = offset / numberofsourcesamplesets_;
1950 size_t source_index = offset % numberofsourcesamplesets_;
1951
1952 GetElementDistribution(isotope_order_[isotope_index], samplesetsorder_[source_index])
1953 ->SetEstimatedSigma(value);
1954
1955 return true;
1956 }
1957
1958 // ========== Block 6: Element Error Standard Deviation ==========
1959 size_t error_element_index = isotope_sigma_end;
1960
1961 if (estimating_contributions && index == error_element_index)
1962 {
1963 if (value < 0.0) {
1964 std::cerr << "Error: Element error std dev cannot be negative (value = "
1965 << value << ")" << std::endl;
1966 return false;
1967 }
1968
1969 error_stdev_ = value;
1970 return true;
1971 }
1972
1973 // ========== Block 7: Isotope Error Standard Deviation ==========
1974 size_t error_isotope_index = error_element_index + 1;
1975
1976 if (estimating_contributions && index == error_isotope_index)
1977 {
1978 if (value < 0.0) {
1979 std::cerr << "Error: Isotope error std dev cannot be negative (value = "
1980 << value << ")" << std::endl;
1981 return false;
1982 }
1983
1984 error_stdev_isotope_ = value;
1985 return true;
1986 }
1987
1988 // If we reach here, index doesn't match any known parameter block
1989 std::cerr << "Warning: Parameter index " << index << " not recognized" << std::endl;
1990 return false;
1991}
1992
1993double SourceSinkData::GetParameterValue(size_t index) const
1994{
1995 return parameters_[index].Value();
1996}
1997
1998bool SourceSinkData::SetParameterValue(const CVector& values)
1999{
2000 bool all_successful = true;
2001
2002 // Update each parameter sequentially
2003 for (size_t i = 0; i < static_cast<size_t>(values.num); i++)
2004 {
2005 bool success = SetParameterValue(i, values[i]);
2006 all_successful &= success;
2007
2008 // Note: Continue even if one fails to attempt all updates
2009 }
2010
2011 return all_successful;
2012}
2013
2015{
2016 CVector parameter_values(parameters_.size());
2017
2018 // Extract value from each parameter
2019 for (size_t i = 0; i < parameters_.size(); i++)
2020 {
2021 parameter_values[i] = parameters_[i].Value();
2022 }
2023
2024 return parameter_values;
2025}
2026
2027CVector SourceSinkData::Gradient(const CVector& parameters, estimation_mode est_mode)
2028{
2029 CVector gradient(parameters.num);
2030 CVector perturbed_params = parameters;
2031
2032 // Set base parameters and evaluate baseline log-likelihood
2033 SetParameterValue(parameters);
2034 double baseline_log_likelihood = LogLikelihood(est_mode);
2035
2036 // Compute partial derivative for each parameter using finite differences
2037 for (size_t i = 0; i < static_cast<size_t>(parameters.num); i++)
2038 {
2039 // Perturb parameter i by epsilon
2040 perturbed_params[i] += epsilon_;
2041 SetParameterValue(perturbed_params);
2042
2043 // Evaluate log-likelihood at perturbed point
2044 double perturbed_log_likelihood = LogLikelihood(est_mode);
2045
2046 // Compute numerical derivative: ∂log(L)/∂θ_i ≈ Δlog(L) / Δθ_i
2047 gradient[i] = (perturbed_log_likelihood - baseline_log_likelihood) / epsilon_;
2048
2049 // Restore parameter for next iteration
2050 perturbed_params[i] = parameters[i];
2051 }
2052
2053 // Normalize gradient to unit length for numerical stability
2054 return gradient / gradient.norm2();
2055}
2056
2058{
2059 // Get current parameters and evaluate baseline likelihood
2060 CVector current_params = GetParameterValue();
2061 double baseline_likelihood = LogLikelihood(est_mode);
2062
2063 // Compute normalized gradient direction
2064 CVector gradient_direction = Gradient(current_params, est_mode);
2065
2066 // Try two candidate steps: standard and aggressive (2×)
2067 CVector candidate_step1 = current_params + distance_coeff_ * gradient_direction;
2068 SetParameterValue(candidate_step1);
2069 double likelihood_step1 = LogLikelihood(est_mode);
2070
2071 CVector candidate_step2 = current_params + 2.0 * distance_coeff_ * gradient_direction;
2072 SetParameterValue(candidate_step2);
2073 double likelihood_step2 = LogLikelihood(est_mode);
2074
2075 // Debug output
2076 std::cout << "Distance Coefficient: " << distance_coeff_ << std::endl;
2077
2078 // Reset step size if it becomes too small
2079 if (distance_coeff_ < 1e-6) {
2080 distance_coeff_ = 1.0;
2081 }
2082
2083 // ========== Strategy 1: Aggressive step (2×) is best ==========
2084 if (likelihood_step2 > likelihood_step1 && likelihood_step2 > baseline_likelihood)
2085 {
2086 // Success with larger step - increase step size for next iteration
2087 distance_coeff_ *= 2.0;
2088 return candidate_step2;
2089 }
2090
2091 // ========== Strategy 2: Standard step (1×) is best ==========
2092 else if (likelihood_step1 > likelihood_step2 && likelihood_step1 > baseline_likelihood)
2093 {
2094 // Success with standard step - keep current step size
2095 SetParameterValue(candidate_step1);
2096 return candidate_step1;
2097 }
2098
2099 // ========== Strategy 3: Neither improved - backtrack ==========
2100 else
2101 {
2102 const int max_backtrack_iterations = 5;
2103 int backtrack_count = 0;
2104
2105 // Progressively reduce step size until we find improvement
2106 while (baseline_likelihood >= likelihood_step1 && backtrack_count < max_backtrack_iterations)
2107 {
2108 distance_coeff_ *= 0.5; // Halve the step size
2109
2110 candidate_step1 = current_params + distance_coeff_ * gradient_direction;
2111 SetParameterValue(candidate_step1);
2112 likelihood_step1 = LogLikelihood(est_mode);
2113
2114 backtrack_count++;
2115 }
2116
2117 if (backtrack_count < max_backtrack_iterations)
2118 {
2119 // Found an improvement with smaller step
2120 SetParameterValue(candidate_step1);
2121 return candidate_step1;
2122 }
2123 else
2124 {
2125 // No improvement found after backtracking - return to original
2126 std::cerr << "Warning: No improvement found after " << max_backtrack_iterations
2127 << " backtracking iterations" << std::endl;
2128 SetParameterValue(current_params);
2129 return current_params;
2130 }
2131 }
2132}
2133
2135{
2136 // Reset counters and ordering
2138 constituent_order_.clear();
2139
2140 // Collect all chemical elements (exclude isotopes, size, organic carbon)
2141 for (const auto& [element_name, elem_info] : element_information_)
2142 {
2143 if (elem_info.Role == element_information::role::element)
2144 {
2146 constituent_order_.push_back(element_name);
2147 }
2148 }
2149
2150 return constituent_order_;
2151}
2152
2154{
2155 // Reset counters and ordering
2157 isotope_order_.clear();
2158
2159 // Collect isotopes marked for inclusion in analysis
2160 for (const auto& [element_name, elem_info] : element_information_)
2161 {
2162 if (elem_info.Role == element_information::role::isotope &&
2163 elem_info.include_in_analysis)
2164 {
2166 isotope_order_.push_back(element_name);
2167 }
2168 }
2169
2170 return isotope_order_;
2171}
2173{
2174 // Reset all counters and ordering vectors
2176 constituent_order_.clear();
2177 element_order_.clear();
2178 size_om_order_.clear();
2179 isotope_order_.clear();
2180
2181 // Pass 1: Collect ALL constituents (elements, isotopes, metadata)
2182 for (const auto& [constituent_name, constituent_info] : element_information_)
2183 {
2185 constituent_order_.push_back(constituent_name);
2186 }
2187
2188 // Pass 2: Collect chemical elements included in analysis
2189 for (const auto& [element_name, elem_info] : element_information_)
2190 {
2191 if (elem_info.Role == element_information::role::element &&
2192 elem_info.include_in_analysis)
2193 {
2194 element_order_.push_back(element_name);
2195 }
2196 }
2197
2198 // Pass 3: Collect isotopes included in analysis
2199 for (const auto& [isotope_name, elem_info] : element_information_)
2200 {
2201 if (elem_info.Role == element_information::role::isotope &&
2202 elem_info.include_in_analysis)
2203 {
2204 isotope_order_.push_back(isotope_name);
2205 }
2206 }
2207
2208 // Pass 4: Collect particle size parameters
2209 for (const auto& [param_name, elem_info] : element_information_)
2210 {
2211 if (elem_info.Role == element_information::role::particle_size)
2212 {
2213 size_om_order_.push_back(param_name);
2214 }
2215 }
2216
2217 // Pass 5: Collect organic carbon parameters
2218 for (const auto& [param_name, elem_info] : element_information_)
2219 {
2220 if (elem_info.Role == element_information::role::organic_carbon)
2221 {
2222 size_om_order_.push_back(param_name);
2223 }
2224 }
2225}
2227{
2228 vector<string> source_names;
2229
2230 // Collect all group names except the target
2231 for (const auto& [group_name, profile_set] : *this)
2232 {
2233 if (group_name != target_group_)
2234 {
2235 source_names.push_back(group_name);
2236 }
2237 }
2238
2239 return source_names;
2240}
2241
2243{
2244 // Search all groups for the specified sample
2245 for (auto& [group_name, profile_set] : *this)
2246 {
2247 // Search within this group's samples
2248 for (const auto& [profile_name, profile] : profile_set)
2249 {
2250 if (profile_name == sample_name)
2251 {
2252 // Found it - return pointer to the profile
2253 return profile_set.GetProfile(profile_name);
2254 }
2255 }
2256 }
2257
2258 // Sample not found in any group
2259 return nullptr;
2260}
2261
2263{
2264 ResultItem result;
2265 Contribution* contributions = new Contribution();
2266
2267 // Package contribution values with source names
2268 vector<string> source_order = GetSourceOrder();
2269 CVector contribution_values = GetContributionVector();
2270
2271 for (size_t i = 0; i < source_order.size(); i++)
2272 {
2273 (*contributions)[source_order[i]] = contribution_values[i];
2274 }
2275
2276 // Configure result item
2277 result.SetName("Contributions");
2278 result.SetResult(contributions);
2280
2281 return result;
2282}
2283
2285{
2286 ResultItem result;
2287 Elemental_Profile* predicted_profile = new Elemental_Profile();
2288
2289 // Compute predicted concentrations using mixing model
2290 CVector predicted_concentrations = PredictTarget(param_mode);
2291 vector<string> element_names = ElementOrder();
2292
2293 // Package predictions into elemental profile
2294 for (size_t i = 0; i < element_names.size(); i++)
2295 {
2296 predicted_profile->AppendElement(element_names[i], predicted_concentrations[i]);
2297 }
2298
2299 // Configure result item
2300 result.SetName("Modeled Elemental Profile");
2301 result.SetResult(predicted_profile);
2303
2304 return result;
2305}
2306
2308{
2309 const size_t num_observations = ObservationsCount();
2310 CVector predicted_values(num_observations);
2311
2312 // Collect predicted value from each observation
2313 for (size_t i = 0; i < num_observations; i++)
2314 {
2315 predicted_values[i] = observation(i)->PredictedValue();
2316 }
2317
2318 return predicted_values;
2319}
2320
2322{
2323 ResultItem result;
2324 Elemental_Profile* predicted_isotopes = new Elemental_Profile();
2325
2326 // Compute predicted delta values using mixing model
2327 CVector predicted_delta_values = PredictTarget_Isotope_delta(param_mode);
2328 vector<string> isotope_names = IsotopeOrder();
2329
2330 // Package predictions into elemental profile
2331 for (size_t i = 0; i < isotope_names.size(); i++)
2332 {
2333 predicted_isotopes->AppendElement(isotope_names[i], predicted_delta_values[i]);
2334 }
2335
2336 // Configure result item
2337 result.SetName("Modeled Elemental Profile for Isotopes");
2338 result.SetResult(predicted_isotopes);
2340
2341 return result;
2342}
2343
2345{
2346 ResultItem result;
2347
2348 // Get predicted and observed profiles
2349 Elemental_Profile* predicted = static_cast<Elemental_Profile*>(
2351 );
2352 Elemental_Profile* observed = static_cast<Elemental_Profile*>(
2354 );
2355
2356 // Create comparison profile set
2357 Elemental_Profile_Set* comparison = new Elemental_Profile_Set();
2358 comparison->AppendProfile("Observed", *observed);
2359 comparison->AppendProfile("Modeled", *predicted);
2360
2361 // Configure result item
2362 result.SetName("Observed vs Modeled Elemental Profile");
2363 result.SetResult(comparison);
2365
2366 return result;
2367}
2368
2370{
2371 vector<ResultItem> mlr_results;
2372
2373 // Collect regression results from each sample group
2374 for (auto& [group_name, profile_set] : *this)
2375 {
2376 // Get MLR models for this group
2377 ResultItem regression_result = profile_set.GetRegressionsAsResult();
2378
2379 // Configure for display
2380 regression_result.SetShowTable(true);
2381 regression_result.SetName("OM & Size MLR for " + group_name);
2382
2383 mlr_results.push_back(regression_result);
2384 }
2385
2386 return mlr_results;
2387}
2388
2390{
2391 ResultItem result;
2392
2393 // Get predicted and observed isotope profiles
2394 Elemental_Profile* predicted = static_cast<Elemental_Profile*>(
2396 );
2397 Elemental_Profile* observed = static_cast<Elemental_Profile*>(
2399 );
2400
2401 // Create comparison profile set
2402 Elemental_Profile_Set* comparison = new Elemental_Profile_Set();
2403 comparison->AppendProfile("Observed", *observed);
2404 comparison->AppendProfile("Modeled", *predicted);
2405
2406 // Configure result item
2407 result.SetName("Observed vs Modeled Elemental Profile for Isotopes");
2408 result.SetResult(comparison);
2410
2411 return result;
2412}
2413
2415{
2416 ResultItem result;
2417 Elemental_Profile* observed_profile = new Elemental_Profile();
2418
2419 // Retrieve observed concentrations for selected target sample
2420 CVector observed_concentrations = ObservedDataforSelectedSample(selected_target_sample_);
2421 vector<string> element_names = ElementOrder();
2422
2423 // Package observations into elemental profile
2424 for (size_t i = 0; i < element_names.size(); i++)
2425 {
2426 observed_profile->AppendElement(element_names[i], observed_concentrations[i]);
2427 }
2428
2429 // Configure result item
2430 result.SetName("Observed Elemental Profile");
2431 result.SetResult(observed_profile);
2433
2434 return result;
2435}
2436
2438{
2439 ResultItem result;
2440 Elemental_Profile* observed_isotopes = new Elemental_Profile();
2441
2442 // Retrieve observed delta values for selected target sample
2444 vector<string> isotope_names = IsotopeOrder();
2445
2446 // Package observations into elemental profile
2447 for (size_t i = 0; i < isotope_names.size(); i++)
2448 {
2449 observed_isotopes->AppendElement(isotope_names[i], observed_delta_values[i]);
2450 }
2451
2452 // Configure result item
2453 result.SetName("Observed Elemental Profile for Isotopes");
2454 result.SetResult(observed_isotopes);
2456
2457 return result;
2458}
2459
2461{
2462 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2463
2464 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2465 {
2466 if (it->first != target_group_)
2467 {
2468 Elemental_Profile element_profile;
2469 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2470 {
2471 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateMean());
2472 }
2473 profile_set->AppendProfile(it->first, element_profile);
2474 }
2475 }
2476
2477 ResultItem resitem;
2478 resitem.SetName("Calculated mean elemental contents");
2480 resitem.SetResult(profile_set);
2481 return resitem;
2482}
2483
2485{
2486 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2487
2488 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2489 {
2490 if (it->first != target_group_)
2491 {
2492 Elemental_Profile element_profile;
2493 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2494 {
2495 if (it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedDistribution()->distribution == distribution_type::normal)
2496 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateStdDev());
2497 else if (it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedDistribution()->distribution == distribution_type::lognormal)
2498 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateStdDevLog());
2499 }
2500 profile_set->AppendProfile(it->first, element_profile);
2501 }
2502 }
2503
2504 ResultItem resitem;
2505 resitem.SetName("Calculated elemental contents standard deviations");
2507 resitem.SetResult(profile_set);
2508 return resitem;
2509}
2510
2511
2513{
2514 vector<ResultItem> source_profile_results;
2515
2516 // Package elemental profiles for each source group
2517 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2518 {
2519 // Skip target group - only process sources
2520 if (it->first != target_group_)
2521 {
2522 // Create and configure result item for this source
2523 ResultItem result_item;
2524 result_item.SetName("Elemental Profiles for " + it->first);
2525 result_item.SetShowAsString(true);
2526 result_item.SetShowTable(true);
2527 result_item.SetShowGraph(true);
2529
2530 // Copy profile set for this source
2531 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2532 *profile_set = it->second;
2533 result_item.SetResult(profile_set);
2534
2535 source_profile_results.push_back(result_item);
2536 }
2537 }
2538
2539 return source_profile_results;
2540}
2541
2542
2544{
2545 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2546
2547 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2548 {
2549 if (it->first != target_group_)
2550 {
2551 Elemental_Profile element_profile;
2552 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2553 {
2554 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->CalculateMeanLog());
2555 }
2556 profile_set->AppendProfile(it->first, element_profile);
2557 }
2558 }
2559
2560 ResultItem resitem;
2561 resitem.SetName("Calculated mu parameter of elemental contents");
2563 resitem.SetResult(profile_set);
2564 return resitem;
2565}
2566
2568{
2569 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2570
2571 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2572 {
2573 if (it->first != target_group_)
2574 {
2575 Elemental_Profile element_profile;
2576 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2577 {
2578 element_profile.AppendElement(element_order_[element_counter], it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedMu());
2579 }
2580 profile_set->AppendProfile(it->first, element_profile);
2581 }
2582 }
2583
2584 ResultItem resitem;
2585 resitem.SetName("Infered mu parameter elemental contents");
2587 resitem.SetResult(profile_set);
2588 return resitem;
2589}
2590
2592{
2593 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2594
2595 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2596 {
2597 if (it->first != target_group_)
2598 {
2599 Elemental_Profile element_profile;
2600 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2601 {
2602 double sigma = it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedSigma();
2603 double mu = it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedMu();
2604 element_profile.AppendElement(element_order_[element_counter], exp(mu + pow(sigma, 2) / 2));
2605 }
2606 profile_set->AppendProfile(it->first, element_profile);
2607 }
2608 }
2609
2610 ResultItem resitem;
2611 resitem.SetName("Infered mean of elemental contents");
2613 resitem.SetResult(profile_set);
2614 return resitem;
2615}
2616
2618{
2619 Elemental_Profile_Set* profile_set = new Elemental_Profile_Set();
2620
2621 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2622 {
2623 if (it->first != target_group_)
2624 {
2625 Elemental_Profile element_profile;
2626 for (unsigned int element_counter = 0; element_counter < element_order_.size(); element_counter++)
2627 {
2628 double sigma = it->second.GetElementDistribution(element_order_[element_counter])->GetEstimatedSigma();
2629 element_profile.AppendElement(element_order_[element_counter], sigma);
2630 }
2631 profile_set->AppendProfile(it->first, element_profile);
2632 }
2633 }
2634
2635 ResultItem resitem;
2636 resitem.SetName("Infered sigma parameter");
2638 resitem.SetResult(profile_set);
2639 return resitem;
2640}
2641
2642
2643
2645{
2646 QJsonArray tools_used_json_array;
2647
2648 // Serialize each tool name
2649 for (list<string>::const_iterator it = tools_used_.cbegin(); it != tools_used_.cend(); it++)
2650 {
2651 tools_used_json_array.append(QString::fromStdString(*it));
2652 }
2653
2654 return tools_used_json_array;
2655}
2656
2658{
2659 QJsonObject json_object;
2660
2661 // Serialize each option name-value pair
2662 for (QMap<QString, double>::const_iterator it = options_.cbegin(); it != options_.cend(); it++)
2663 {
2664 json_object[it.key()] = it.value();
2665 }
2666
2667 return json_object;
2668}
2669
2670void SourceSinkData::AddtoToolsUsed(const string& tool)
2671{
2672 // Only add if not already present
2673 if (!ToolsUsed(tool))
2674 {
2675 tools_used_.push_back(tool);
2676 }
2677}
2678
2679bool SourceSinkData::ReadToolsUsedFromJsonObject(const QJsonArray& jsonarray)
2680{
2681 // Deserialize each tool name
2682 foreach (const QJsonValue& value, jsonarray)
2683 {
2684 AddtoToolsUsed(value.toString().toStdString());
2685 }
2686
2687 return true;
2688}
2689
2691{
2692 // Clear existing element information
2693 element_information_.clear();
2694
2695 // Deserialize metadata for each element
2696 for (QString key : jsonobject.keys())
2697 {
2698 element_information elem_info;
2699 elem_info.Role = Role(jsonobject[key].toObject()["Role"].toString());
2700 elem_info.standard_ratio = jsonobject[key].toObject()["Standard Ratio"].toDouble();
2701 elem_info.base_element = jsonobject[key].toObject()["Base Element"].toString().toStdString();
2702 elem_info.include_in_analysis = jsonobject[key].toObject()["Include"].toBool();
2703
2704 element_information_[key.toStdString()] = elem_info;
2705 }
2706
2707 return true;
2708}
2709
2710bool SourceSinkData::ReadElementDatafromJsonObject(const QJsonObject& jsonobject)
2711{
2712 // Clear existing data
2713 clear();
2714
2715 // Deserialize each sample group
2716 for (QString key : jsonobject.keys())
2717 {
2719 elemental_profile_set.ReadFromJsonObject(jsonobject[key].toObject());
2720 operator[](key.toStdString()) = elemental_profile_set;
2721 }
2722
2723 return true;
2724}
2725
2726bool SourceSinkData::ReadOptionsfromJsonObject(const QJsonObject& jsonobject)
2727{
2728 // Deserialize each option
2729 for (QString key : jsonobject.keys())
2730 {
2731 options_[key] = jsonobject[key].toDouble();
2732 }
2733
2734 return true;
2735}
2736
2738{
2739 QJsonObject json_object;
2740
2741 // Serialize each sample group
2742 for (map<string, Elemental_Profile_Set>::const_iterator it = cbegin(); it != cend(); it++)
2743 {
2744 json_object[QString::fromStdString(it->first)] = it->second.toJsonObject();
2745 }
2746
2747 return json_object;
2748}
2749
2751{
2752 // Write header
2753 file->write("***\n");
2754 file->write("Elemental Profiles\n");
2755
2756 // Write each sample group
2757 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2758 {
2759 file->write("**\n");
2760 file->write(QString::fromStdString(it->first + "\n").toUtf8());
2761 it->second.writetofile(file);
2762 }
2763
2764 return true;
2765}
2766
2768{
2769 return WriteDataToFile(file);
2770}
2771
2773{
2774 // Clear existing data
2775 Clear();
2776
2777 // Parse JSON from file
2778 QJsonObject jsondoc = QJsonDocument().fromJson(fil->readAll()).object();
2779
2780 // Load all components
2781 ReadElementDatafromJsonObject(jsondoc["Element Data"].toObject());
2782 ReadElementInformationfromJsonObject(jsondoc["Element Information"].toObject());
2783 ReadToolsUsedFromJsonObject(jsondoc["Tools used"].toArray());
2784 ReadOptionsfromJsonObject(jsondoc["Options"].toObject());
2785 target_group_ = jsondoc["Target Group"].toString().toStdString();
2786
2787 return true;
2788}
2789
2791{
2792 QJsonObject json_object;
2793
2794 // Serialize metadata for each element
2795 for (map<string, element_information>::const_iterator it = element_information_.cbegin(); it != element_information_.cend(); it++)
2796 {
2797 QJsonObject elem_info_json_obj;
2798 elem_info_json_obj["Role"] = Role(it->second.Role);
2799 elem_info_json_obj["Standard Ratio"] = it->second.standard_ratio;
2800 elem_info_json_obj["Base Element"] = QString::fromStdString(it->second.base_element);
2801 elem_info_json_obj["Include"] = it->second.include_in_analysis;
2802
2803 json_object[QString::fromStdString(it->first)] = elem_info_json_obj;
2804 }
2805
2806 return json_object;
2807}
2808
2810{
2811 // Convert enum to string representation
2813 return "DoNotInclude";
2814 else if (role == element_information::role::element)
2815 return "Element";
2816 else if (role == element_information::role::isotope)
2817 return "Isotope";
2819 return "ParticleSize";
2821 return "OM";
2822
2823 // Default for unrecognized values
2824 return "DoNotInclude";
2825}
2826
2827element_information::role SourceSinkData::Role(const QString& role_string) const
2828{
2829 // Convert string to enum representation
2830 if (role_string == "DoNotInclude")
2832 else if (role_string == "Element")
2834 else if (role_string == "Isotope")
2836 else if (role_string == "ParticleSize")
2838 else if (role_string == "OM")
2840
2841 // Default for unrecognized values
2843}
2844
2846 const string& om,
2847 const string& particle_size,
2848 regression_form form,
2849 const double& p_value_threshold)
2850{
2851 // Store OM and size constituent names for later use in corrections
2852 omconstituent_ = om;
2853 sizeconsituent_ = particle_size;
2854 regression_p_value_threshold_ = p_value_threshold;
2855
2856 // Compute regression models for all sample groups
2857 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2858 {
2859 it->second.SetRegressionModels(om, particle_size, form, p_value_threshold);
2860 }
2861
2862 return true;
2863}
2864
2865void SourceSinkData::OutlierAnalysisForAll(const double& lower_threshold, const double& upper_threshold)
2866{
2867 // Perform outlier detection on each source group
2868 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
2869 {
2870 // Skip target group - only analyze sources
2871 if (it->first != target_group_)
2872 {
2873 it->second.DetectOutliers(lower_threshold, upper_threshold);
2874 }
2875 }
2876}
2877
2879{
2880 DFA_result result;
2881
2882 // Initialize result vectors for all sources
2883 const size_t num_sources = size() - 1; // Exclude target
2884 result.F_test_P_value = CMBVector(num_sources);
2885 result.p_values = CMBVector(num_sources);
2886 result.wilkslambda = CMBVector(num_sources);
2887
2888 // Perform DFA for each source vs all others
2889 size_t counter = 0;
2890 for (map<string, Elemental_Profile_Set>::iterator source = begin(); source != end(); source++)
2891 {
2892 // Skip target group
2893 if (source->first != target_group_)
2894 {
2895 // Run one-vs-rest DFA for this source
2896 DFA_result source_result = DiscriminantFunctionAnalysis(source->first);
2897
2898 // Extract and store results
2899 result.F_test_P_value[counter] = source_result.F_test_P_value[0];
2900 result.F_test_P_value.SetLabel(counter, source->first);
2901
2902 result.p_values[counter] = source_result.p_values[0];
2903 result.p_values.SetLabel(counter, source->first);
2904
2905 result.wilkslambda[counter] = source_result.wilkslambda[0];
2906 result.wilkslambda.SetLabel(counter, source->first);
2907
2908 result.eigen_vectors.Append(source->first, source_result.eigen_vectors[source->first + " vs the rest"]);
2909 result.multi_projected.Append(source->first, source_result.projected);
2910
2911 counter++;
2912 }
2913 }
2914
2915 return result;
2916}
2917
2918DFA_result SourceSinkData::DiscriminantFunctionAnalysis(const string& source1, const string& source2)
2919{
2920 // Create temporary dataset with only two sources
2921 SourceSinkData two_sources;
2922 two_sources.AppendSampleSet(source1, at(source1));
2923 two_sources.AppendSampleSet(source2, at(source2));
2925
2926 DFA_result result;
2927
2928 // Compute discriminant eigenvector
2929 CMBVector eigen_vector = two_sources.DFA_eigvector();
2930
2931 // Return empty result if eigenvector computation failed
2932 if (eigen_vector.size() == 0)
2933 {
2934 return result;
2935 }
2936
2937 // Project samples onto discriminant axis
2938 result.projected = two_sources.DFA_Projected(source1, source2);
2939
2940 // Compute F-test p-value for separation
2941 result.F_test_P_value = CMBVector(1);
2942 result.F_test_P_value[0] = result.projected.FTest_p_value();
2943
2944 // Store eigenvector
2945 result.eigen_vectors.Append(source1 + " vs " + source2, eigen_vector);
2946
2947 // Compute discriminant p-value and Wilks' Lambda
2948 double p_value = two_sources.DFA_P_Value();
2949 result.p_values = CMBVector(1);
2950 result.p_values[0] = p_value;
2951
2952 result.wilkslambda = CMBVector(1);
2953 result.wilkslambda[0] = two_sources.WilksLambda();
2954
2955 return result;
2956}
2957
2959{
2960 // Create temporary dataset with one source vs all others
2961 SourceSinkData two_sources;
2962 two_sources.AppendSampleSet(source1, at(source1));
2963 two_sources.AppendSampleSet("Others", TheRest(source1));
2965
2966 DFA_result result;
2967
2968 // Compute discriminant eigenvector
2969 CMBVector eigen_vector = two_sources.DFA_eigvector();
2970
2971 // Project all samples onto discriminant axis
2972 result.projected = two_sources.DFA_Projected(source1, this);
2973
2974 // Project pairwise comparison (source vs others)
2975 CMBVectorSet pairwise_projected = two_sources.DFA_Projected(source1, "Others");
2976
2977 // Compute F-test p-value from pairwise projection
2978 result.F_test_P_value = CMBVector(1);
2979 result.F_test_P_value[0] = pairwise_projected.FTest_p_value();
2980
2981 // Store eigenvector with descriptive label
2982 result.eigen_vectors.Append(source1 + " vs the rest", eigen_vector);
2983
2984 // Compute discriminant p-value and Wilks' Lambda
2985 double p_value = two_sources.DFA_P_Value();
2986 result.p_values = CMBVector(1);
2987 result.p_values[0] = p_value;
2988
2989 result.wilkslambda = CMBVector(1);
2990 result.wilkslambda[0] = two_sources.WilksLambda();
2991
2992 return result;
2993}
2995{
2996 int count = 0;
2997
2998 // Sum samples across all source groups
2999 for (map<string, Elemental_Profile_Set>::const_iterator source_group = cbegin(); source_group != cend(); source_group++)
3000 {
3001 // Skip target group - only count source samples
3002 if (source_group->first != target_group_)
3003 {
3004 count += source_group->second.size();
3005 }
3006 }
3007
3008 return count;
3009}
3010
3011CMBVector SourceSinkData::DFATransformed(const CMBVector& eigenvector, const string& source_group)
3012{
3013 // Initialize result vector for discriminant scores
3014 CMBVector discriminant_scores(operator[](source_group).size());
3015
3016 // Project each sample onto the discriminant axis
3017 size_t sample_index = 0;
3018 for (map<string, Elemental_Profile>::iterator sample = operator[](source_group).begin();
3019 sample != operator[](source_group).end();
3020 sample++)
3021 {
3022 // Compute dot product: discriminant score = eigenvector · sample_profile
3023 discriminant_scores[sample_index] = sample->second.CalculateDotProduct(eigenvector);
3024 discriminant_scores.SetLabel(sample_index, sample->first);
3025
3026 sample_index++;
3027 }
3028
3029 return discriminant_scores;
3030}
3031
3033{
3034 Elemental_Profile_Set combined_sources;
3035
3036 // Iterate through all sample groups
3037 for (map<string, Elemental_Profile_Set>::iterator profile_set = begin();
3038 profile_set != end();
3039 profile_set++)
3040 {
3041 // Skip the excluded source and target group
3042 if (profile_set->first != excluded_source && profile_set->first != target_group_)
3043 {
3044 // Add all samples from this source group
3045 for (map<string, Elemental_Profile>::iterator profile = profile_set->second.begin();
3046 profile != profile_set->second.end();
3047 profile++)
3048 {
3049 combined_sources.AppendProfile(profile->first, profile->second);
3050 }
3051 }
3052 }
3053
3054 return combined_sources;
3055}
3056
3057
3058CMBVector SourceSinkData::BracketTest(const string& target_sample, bool correct_based_on_om_n_size)
3059{
3060 vector<string> element_names = GetElementNames();
3061 const size_t num_elements = element_names.size();
3062
3063 // Initialize result vectors
3064 CMBVector bracket_test_results(num_elements);
3065 CVector exceeds_max(num_elements); // Flags for concentrations above source maximum
3066 CVector below_min(num_elements); // Flags for concentrations below source minimum
3067
3068 exceeds_max.SetAllValues(1.0); // Assume all exceed until proven otherwise
3069 below_min.SetAllValues(1.0); // Assume all below until proven otherwise
3070
3071 // Create corrected dataset for testing
3073 false, false, correct_based_on_om_n_size, target_sample);
3074
3075 // Check each source group's concentration ranges
3076 for (map<string, Elemental_Profile_Set>::iterator it = corrected_data.begin();
3077 it != corrected_data.end();
3078 it++)
3079 {
3080 // Skip target group
3081 if (it->first != target_group_)
3082 {
3083 // Check each element
3084 for (size_t i = 0; i < num_elements; i++)
3085 {
3086 bracket_test_results.SetLabel(i, element_names[i]);
3087
3088 double target_concentration = corrected_data.at(target_group_)
3089 .GetProfile(target_sample)->at(element_names[i]);
3090 double source_maximum = it->second.GetElementDistribution(element_names[i])
3091 ->GetMaximum();
3092 double source_minimum = it->second.GetElementDistribution(element_names[i])
3093 ->GetMinimum();
3094
3095 // Check if target is within this source's range
3096 if (target_concentration <= source_maximum)
3097 {
3098 exceeds_max[i] = 0; // Target does not exceed max
3099 }
3100 if (target_concentration >= source_minimum)
3101 {
3102 below_min[i] = 0; // Target is not below min
3103 }
3104 }
3105 }
3106 }
3107
3108 // Compile results and annotate failures
3109 for (size_t i = 0; i < num_elements; i++)
3110 {
3111 // Fail if either above all maxima or below all minima
3112 bracket_test_results[i] = max(exceeds_max[i], below_min[i]);
3113
3114 // Annotate failures in target sample notes
3115 if (exceeds_max[i] > 0.5)
3116 {
3117 at(target_group_).GetProfile(target_sample)->AppendtoNotes(
3118 element_names[i] + " value is higher than the maximum of the sources");
3119 }
3120 else if (below_min[i] > 0.5)
3121 {
3122 at(target_group_).GetProfile(target_sample)->AppendtoNotes(
3123 element_names[i] + " value is lower than the minimum of the sources");
3124 }
3125 }
3126
3127 return bracket_test_results;
3128}
3129
3130
3132 bool correct_based_on_om_n_size,
3133 bool exclude_elements,
3134 bool exclude_samples)
3135{
3136 // Initialize result matrix
3137 // Note: Matrix is constructed as (num_rows, num_cols) but accessed as [row][col]
3138 const size_t num_target_samples = at(target_group_).size();
3139 const size_t num_elements = CountElements(exclude_elements);
3140 CMBMatrix bracket_results(num_target_samples, num_elements);
3141
3142 // Test each target sample
3143 size_t sample_index = 0;
3144 for (map<string, Elemental_Profile>::iterator sample = at(target_group_).begin();
3145 sample != at(target_group_).end();
3146 sample++)
3147 {
3148 // Prepare dataset for this target sample
3149 SourceSinkData corrected_data;
3150 if (correct_based_on_om_n_size)
3151 {
3152 corrected_data = CreateCorrectedAndFilteredDataset(
3153 exclude_samples, exclude_elements, true, sample->first);
3154 }
3155 else
3156 {
3157 corrected_data = CreateCorrectedAndFilteredDataset(
3158 exclude_samples, exclude_elements, false);
3159 }
3160
3161 // Perform bracket test for this sample
3162 vector<string> element_names = corrected_data.GetElementNames();
3163 CMBVector bracket_vector = corrected_data.BracketTest(sample->first, false);
3164
3165 // Store results in matrix
3166 for (size_t element_index = 0; element_index < element_names.size(); element_index++)
3167 {
3168 bracket_results[element_index][sample_index] = bracket_vector.valueAt(element_index);
3169 bracket_results.SetRowLabel(element_index, element_names[element_index]);
3170 }
3171 bracket_results.SetColumnLabel(sample_index, sample->first);
3172
3173 sample_index++;
3174 }
3175
3176 return bracket_results;
3177}
3178
3179
3180
3182{
3183 // Compute optimal lambda parameters for each element
3184 CMBVector lambda_parameters = OptimalBoxCoxParameters();
3185
3186 // Create copy of data for transformation
3187 SourceSinkData transformed_data(*this);
3188
3189 // Apply Box-Cox transformation to each source group
3190 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
3191 {
3192 // Skip target group - only transform sources
3193 if (it->first != target_group_)
3194 {
3195 if (calculate_optimal_lambda)
3196 {
3197 // Transform using optimal lambda parameters
3198 transformed_data[it->first] = it->second.ApplyBoxCoxTransform(&lambda_parameters);
3199 }
3200 else
3201 {
3202 // Transform using default parameters
3203 transformed_data[it->first] = it->second.ApplyBoxCoxTransform();
3204 }
3205 }
3206 }
3207
3208 // Recalculate distributions for transformed data
3209 transformed_data.PopulateElementDistributions();
3210 transformed_data.AssignAllDistributions();
3211
3212 return transformed_data;
3213}
3214
3215map<string, ConcentrationSet> SourceSinkData::ExtractConcentrationSet()
3216{
3217 map<string, ConcentrationSet> element_concentrations;
3218 vector<string> element_names = GetElementNames();
3219
3220 // Build concentration set for each element
3221 for (size_t i = 0; i < element_names.size(); i++)
3222 {
3223 ConcentrationSet concentration_set;
3224
3225 // Collect concentrations from all source groups
3226 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != end(); it++)
3227 {
3228 // Skip target group - only collect from sources
3229 if (it->first != target_group_)
3230 {
3231 // Add each sample's concentration for this element
3232 for (map<string, Elemental_Profile>::iterator sample = it->second.begin();
3233 sample != it->second.end();
3234 sample++)
3235 {
3236 concentration_set.AppendValue(sample->second[element_names[i]]);
3237 }
3238 }
3239 }
3240
3241 element_concentrations[element_names[i]] = concentration_set;
3242 }
3243
3244 return element_concentrations;
3245}
3246
3248{
3249 // Extract concentration distributions for all elements
3250 map<string, ConcentrationSet> concentration_sets = ExtractConcentrationSet();
3251 vector<string> element_names = GetElementNames();
3252
3253 // Compute optimal lambda for each element
3254 CMBVector lambda_parameters(element_names.size());
3255
3256 for (size_t i = 0; i < element_names.size(); i++)
3257 {
3258 // Find optimal lambda using maximum likelihood estimation
3259 // Search range: [-5, 5], refinement iterations: 10
3260 lambda_parameters[i] = concentration_sets[element_names[i]].FindOptimalBoxCoxParameter(-5, 5, 10);
3261 }
3262
3263 // Label parameters with element names
3264 lambda_parameters.SetLabels(element_names);
3265
3266 return lambda_parameters;
3267}
3268Elemental_Profile SourceSinkData::t_TestPValue(const string& source1, const string& source2, bool use_log)
3269{
3270 vector<string> element_names = GetElementNames();
3271 Elemental_Profile p_values;
3272
3273 // Perform t-test for each element
3274 for (size_t i = 0; i < element_names.size(); i++)
3275 {
3276 // Get concentration distributions for both sources
3277 ConcentrationSet concentration_set1 = *at(source1).GetElementDistribution(element_names[i]);
3278 ConcentrationSet concentration_set2 = *at(source2).GetElementDistribution(element_names[i]);
3279
3280 // Compute statistics in appropriate space
3281 double std1, std2, mean1, mean2;
3282 if (!use_log)
3283 {
3284 // Linear-space statistics
3285 std1 = concentration_set1.CalculateStdDev();
3286 std2 = concentration_set2.CalculateStdDev();
3287 mean1 = concentration_set1.CalculateMean();
3288 mean2 = concentration_set2.CalculateMean();
3289 }
3290 else
3291 {
3292 // Log-space statistics
3293 std1 = concentration_set1.CalculateStdDevLog();
3294 std2 = concentration_set2.CalculateStdDevLog();
3295 mean1 = concentration_set1.CalculateMeanLog();
3296 mean2 = concentration_set2.CalculateMeanLog();
3297 }
3298
3299 // Compute t-statistic
3300 double standard_error = sqrt(pow(std1, 2) / concentration_set1.size() +
3301 pow(std2, 2) / concentration_set2.size());
3302 double t_statistic = (mean1 - mean2) / standard_error;
3303
3304 // Compute two-tailed p-value
3305 size_t degrees_of_freedom = concentration_set1.size() + concentration_set2.size() - 2;
3306 double p_value_upper = gsl_cdf_tdist_Q(t_statistic, degrees_of_freedom);
3307 double p_value_lower = gsl_cdf_tdist_P(t_statistic, degrees_of_freedom);
3308
3309 // Two-tailed: use minimum of tails, then sum
3310 p_value_upper = min(p_value_upper, 1.0 - p_value_upper);
3311 p_value_lower = min(p_value_lower, 1.0 - p_value_lower);
3312 double p_value = p_value_upper + p_value_lower;
3313
3314 p_values.AppendElement(element_names[i], p_value);
3315 }
3316
3317 return p_values;
3318}
3319
3320Elemental_Profile SourceSinkData::DifferentiationPower(const string& source1, const string& source2, bool use_log)
3321{
3322 vector<string> element_names = GetElementNames();
3323 Elemental_Profile differentiation_powers;
3324
3325 // Compute differentiation power for each element
3326 for (size_t i = 0; i < element_names.size(); i++)
3327 {
3328 // Get concentration distributions for both sources
3329 ConcentrationSet concentration_set1 = *at(source1).GetElementDistribution(element_names[i]);
3330 ConcentrationSet concentration_set2 = *at(source2).GetElementDistribution(element_names[i]);
3331
3332 // Compute statistics in appropriate space
3333 double std1, std2, mean1, mean2;
3334 if (!use_log)
3335 {
3336 // Linear-space statistics
3337 std1 = concentration_set1.CalculateStdDev();
3338 std2 = concentration_set2.CalculateStdDev();
3339 mean1 = concentration_set1.CalculateMean();
3340 mean2 = concentration_set2.CalculateMean();
3341 }
3342 else
3343 {
3344 // Log-space statistics
3345 std1 = concentration_set1.CalculateStdDevLog();
3346 std2 = concentration_set2.CalculateStdDevLog();
3347 mean1 = concentration_set1.CalculateMeanLog();
3348 mean2 = concentration_set2.CalculateMeanLog();
3349 }
3350
3351 // Compute differentiation power: 2 × |Δμ| / (σ₁ + σ₂)
3352 double diff_power = 2.0 * fabs(mean1 - mean2) / (std1 + std2);
3353
3354 differentiation_powers.AppendElement(element_names[i], diff_power);
3355 }
3356
3357 return differentiation_powers;
3358}
3359
3361{
3362 Elemental_Profile_Set all_pairs;
3363
3364 // Compare all pairs of source groups
3365 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3366 {
3367 if (include_target || it->first != target_group_)
3368 {
3369 for (map<string, Elemental_Profile_Set>::iterator it2 = next(it); it2 != end(); it2++)
3370 {
3371 if (include_target || it2->first != target_group_)
3372 {
3373 Elemental_Profile diff_power = DifferentiationPower(it->first, it2->first, use_log);
3374 all_pairs.AppendProfile(it->first + " and " + it2->first, diff_power);
3375 }
3376 }
3377 }
3378 }
3379
3380 return all_pairs;
3381}
3382
3383Elemental_Profile SourceSinkData::DifferentiationPower_Percentage(const string& source1, const string& source2)
3384{
3385 vector<string> element_names = GetElementNames();
3386 Elemental_Profile classification_percentages;
3387
3388 // Compute classification percentage for each element
3389 for (size_t i = 0; i < element_names.size(); i++)
3390 {
3391 // Get concentration distributions
3392 ConcentrationSet concentration_set1 = *at(source1).GetElementDistribution(element_names[i]);
3393 ConcentrationSet concentration_set2 = *at(source2).GetElementDistribution(element_names[i]);
3394
3395 // Pool all samples and compute ranks
3396 ConcentrationSet combined_set = concentration_set1;
3397 combined_set.AppendSet(concentration_set2);
3398 vector<unsigned int> ranks = combined_set.CalculateRanks();
3399
3400 // Count correct classifications
3401 size_t n1 = concentration_set1.size();
3402 int set1_below_limit = aquiutils::CountLessThan(ranks, n1, n1, false);
3403 int set1_above_limit = aquiutils::CountGreaterThan(ranks, n1, n1, false);
3404 int set2_below_limit = aquiutils::CountLessThan(ranks, n1, n1, true);
3405 int set2_above_limit = aquiutils::CountGreaterThan(ranks, n1, n1, true);
3406
3407 // Compute classification success rates
3408 double set1_fraction = double(set1_below_limit + set2_above_limit) / double(combined_set.size());
3409 double set2_fraction = double(set1_above_limit + set2_below_limit) / double(combined_set.size());
3410
3411 // Use best classification direction
3412 double classification_percentage = max(set1_fraction, set2_fraction);
3413
3414 classification_percentages.AppendElement(element_names[i], classification_percentage);
3415 }
3416
3417 return classification_percentages;
3418}
3419
3421{
3422 Elemental_Profile_Set all_pairs;
3423
3424 // Compare all pairs of source groups
3425 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3426 {
3427 if (include_target || it->first != target_group_)
3428 {
3429 for (map<string, Elemental_Profile_Set>::iterator it2 = next(it); it2 != end(); it2++)
3430 {
3431 if (include_target || it2->first != target_group_)
3432 {
3433 Elemental_Profile diff_power_pct = DifferentiationPower_Percentage(it->first, it2->first);
3434 all_pairs.AppendProfile(it->first + " and " + it2->first, diff_power_pct);
3435 }
3436 }
3437 }
3438 }
3439
3440 return all_pairs;
3441}
3442
3444{
3445 Elemental_Profile_Set all_pairs;
3446
3447 // Compare all pairs of source groups
3448 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3449 {
3450 if (include_target || it->first != target_group_)
3451 {
3452 for (map<string, Elemental_Profile_Set>::iterator it2 = next(it); it2 != end(); it2++)
3453 {
3454 if (include_target || it2->first != target_group_)
3455 {
3456 Elemental_Profile p_values = t_TestPValue(it->first, it2->first, false);
3457 all_pairs.AppendProfile(it->first + " and " + it2->first, p_values);
3458 }
3459 }
3460 }
3461 }
3462
3463 return all_pairs;
3464}
3465
3467{
3468 vector<string> error_messages;
3469
3470 // Ensure element ordering is current
3472
3473 // Check each source group for negative values
3474 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3475 {
3476 vector<string> negative_elements = it->second.CheckForNegativeValues(element_order_);
3477
3478 // Generate error message for each problematic element
3479 for (size_t i = 0; i < negative_elements.size(); i++)
3480 {
3481 error_messages.push_back("There are zero or negative values for element '" +
3482 negative_elements[i] + "' in sample group '" +
3483 it->first + "'");
3484 }
3485 }
3486
3487 return error_messages;
3488}
3489
3490void SourceSinkData::IncludeExcludeAllElements(bool include_in_analysis)
3491{
3492 // Set inclusion flag for all elements
3493 for (map<string, element_information>::iterator it = element_information_.begin();
3494 it != element_information_.end();
3495 it++)
3496 {
3497 it->second.include_in_analysis = include_in_analysis;
3498 }
3499}
3500
3501double SourceSinkData::GrandMean(const string& element, bool use_log)
3502{
3503 double weighted_sum = 0.0;
3504 double total_count = 0.0;
3505
3506 // Accumulate weighted means from each source group
3507 for (map<string, Elemental_Profile_Set>::iterator it = begin(); it != prev(end()); it++)
3508 {
3509 // Skip target group
3510 if (it->first != target_group_)
3511 {
3512 ConcentrationSet* element_dist = it->second.GetElementDistribution(element);
3513 size_t sample_count = element_dist->size();
3514
3515 if (!use_log)
3516 {
3517 // Linear-space mean
3518 weighted_sum += element_dist->CalculateMean() * sample_count;
3519 }
3520 else
3521 {
3522 // Log-space mean
3523 weighted_sum += element_dist->CalculateMeanLog() * sample_count;
3524 }
3525
3526 total_count += sample_count;
3527 }
3528 }
3529
3530 // Compute weighted average
3531 return weighted_sum / total_count;
3532}
3533
3535{
3536 Elemental_Profile_Set combined_sources;
3537
3538 // Pool samples from all source groups
3539 for (map<string, Elemental_Profile_Set>::const_iterator it = begin(); it != prev(end()); it++)
3540 {
3541 // Skip target group
3542 if (it->first != target_group_)
3543 {
3544 combined_sources.AppendProfiles(it->second, nullptr);
3545 }
3546 }
3547
3548 // Recalculate distributions for combined dataset
3549 combined_sources.UpdateElementDistributions();
3550
3551 return combined_sources;
3552}
3553
3555{
3556 vector<string> element_names = GetElementNames();
3557 CMBVector p_values(element_names.size());
3558 p_values.SetLabels(element_names);
3559
3560 // Perform ANOVA for each element
3561 for (size_t i = 0; i < element_names.size(); i++)
3562 {
3563 ANOVA_info anova_result = ANOVA(element_names[i], use_log);
3564 p_values[i] = anova_result.p_value;
3565 }
3566
3567 return p_values;
3568}
3569
3570ANOVA_info SourceSinkData::ANOVA(const string& element, bool use_log)
3571{
3572 ANOVA_info anova;
3573
3574 // Combine all source samples for total variance calculation
3576 ConcentrationSet* all_element_data = all_sources.GetElementDistribution(element);
3577
3578 if (!use_log)
3579 {
3580 // Linear-space ANOVA
3581
3582 // Total sum of squares (SST)
3583 anova.SST = all_element_data->CalculateSSE();
3584
3585 // Between-group sum of squares (SSB) and within-group sum of squares (SSW)
3586 double grand_mean = all_element_data->CalculateMean();
3587 double sum_between = 0.0;
3588 double sum_within = 0.0;
3589
3590 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
3591 source_group != end();
3592 source_group++)
3593 {
3594 if (source_group->first != target_group_)
3595 {
3596 ConcentrationSet* group_data = source_group->second.GetElementDistribution(element);
3597 double group_mean = group_data->CalculateMean();
3598 size_t group_size = group_data->size();
3599
3600 // SSB: variance of group means weighted by sample size
3601 sum_between += pow(group_mean - grand_mean, 2) * group_size;
3602
3603 // SSW: sum of within-group variances
3604 sum_within += group_data->CalculateSSE();
3605 }
3606 }
3607
3608 anova.SSB = sum_between;
3609 anova.SSW = sum_within;
3610 }
3611 else
3612 {
3613 // Log-space ANOVA
3614
3615 // Total sum of squares (SST)
3616 double total_std_log = all_element_data->CalculateStdDevLog();
3617 size_t total_size = all_element_data->size();
3618 anova.SST = pow(total_std_log, 2) * (total_size - 1);
3619
3620 // Between-group sum of squares (SSB) and within-group sum of squares (SSW)
3621 double grand_mean_log = all_element_data->CalculateMeanLog();
3622 double sum_between = 0.0;
3623 double sum_within = 0.0;
3624
3625 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
3626 source_group != end();
3627 source_group++)
3628 {
3629 if (source_group->first != target_group_)
3630 {
3631 ConcentrationSet* group_data = source_group->second.GetElementDistribution(element);
3632 double group_mean_log = group_data->CalculateMeanLog();
3633 size_t group_size = group_data->size();
3634
3635 // SSB: variance of group means weighted by sample size
3636 sum_between += pow(group_mean_log - grand_mean_log, 2) * group_size;
3637
3638 // SSW: sum of within-group variances
3639 sum_within += group_data->CalculateSSELog();
3640 }
3641 }
3642
3643 anova.SSB = sum_between;
3644 anova.SSW = sum_within;
3645 }
3646
3647 // Compute mean squares and F-statistic
3648 size_t num_groups = this->size() - 1; // Exclude target
3649 size_t total_samples = all_element_data->size();
3650
3651 // Degrees of freedom
3652 size_t df_between = num_groups - 1;
3653 size_t df_within = total_samples - num_groups;
3654
3655 // Mean squares
3656 anova.MSB = anova.SSB / double(df_between);
3657 anova.MSW = anova.SSW / double(df_within);
3658
3659 // F-statistic
3660 anova.F = anova.MSB / anova.MSW;
3661
3662 // P-value from F-distribution
3663 anova.p_value = gsl_cdf_fdist_Q(anova.F, df_between, total_samples);
3664
3665 return anova;
3666}
3667void SourceSinkData::IncludeExcludeElementsBasedOn(const vector<string>& elements)
3668{
3669 // First, exclude all elements
3670 for (map<string, element_information>::iterator element = element_information_.begin();
3671 element != element_information_.end();
3672 element++)
3673 {
3674 element->second.include_in_analysis = false;
3675 }
3676
3677 // Then, include only specified elements
3678 for (size_t i = 0; i < elements.size(); i++)
3679 {
3680 if (element_information_.count(elements[i]) > 0)
3681 {
3682 element_information_[elements[i]].include_in_analysis = true;
3683 }
3684 }
3685}
3686
3688{
3689 SourceSinkData reduced_dataset;
3690
3691 // Randomly select samples to eliminate
3692 vector<string> samples_to_eliminate = RandomlypickSamples(percentage / 100.0);
3693
3694 // Copy groups with specified samples eliminated
3695 for (map<string, Elemental_Profile_Set>::const_iterator it = cbegin(); it != cend(); it++)
3696 {
3697 if (it->first != target_group_)
3698 {
3699 // Remove randomly selected samples from source groups
3700 reduced_dataset[it->first] = it->second.EliminateSamples(
3701 samples_to_eliminate, &element_information_);
3702 }
3703 else
3704 {
3705 // Preserve target group unchanged
3706 reduced_dataset[it->first] = it->second;
3707 }
3708 }
3709
3710 // Copy metadata and settings
3711 reduced_dataset.omconstituent_ = omconstituent_;
3712 reduced_dataset.sizeconsituent_ = sizeconsituent_;
3713 reduced_dataset.target_group_ = target_group_;
3714
3715 // Recalculate distributions for reduced dataset
3717 reduced_dataset.PopulateElementDistributions();
3718 reduced_dataset.AssignAllDistributions();
3719
3720 return reduced_dataset;
3721}
3722
3723Elemental_Profile SourceSinkData::Sample(const string& sample_name) const
3724{
3725 // Search all groups for the specified sample
3726 for (map<string, Elemental_Profile_Set>::const_iterator it = cbegin(); it != cend(); it++)
3727 {
3728 if (it->second.count(sample_name) == 1)
3729 {
3730 return it->second.at(sample_name);
3731 }
3732 }
3733
3734 // Sample not found - return empty profile
3735 return Elemental_Profile();
3736}
3737SourceSinkData SourceSinkData::ReplaceSourceAsTarget(const string& source_sample_name) const
3738{
3739 // Create copy of current dataset
3740 SourceSinkData modified_dataset = *this;
3741
3742 // Extract specified source sample
3743 Elemental_Profile target_profile = Sample(source_sample_name);
3744
3745 // Create new target group with this sample
3746 Elemental_Profile_Set new_target_group = Elemental_Profile_Set();
3747 new_target_group.AppendProfile(source_sample_name, target_profile);
3748 modified_dataset[target_group_] = new_target_group;
3749
3750 // Copy metadata and settings
3751 modified_dataset.omconstituent_ = omconstituent_;
3752 modified_dataset.sizeconsituent_ = sizeconsituent_;
3753 modified_dataset.target_group_ = target_group_;
3754
3755 // Recalculate distributions
3757 modified_dataset.PopulateElementDistributions();
3758 modified_dataset.AssignAllDistributions();
3759
3760 return modified_dataset;
3761}
3762
3764 const double& percentage,
3765 unsigned int num_iterations,
3766 string target_sample,
3767 bool use_softmax)
3768{
3769 // Initialize result time series
3770 CMBTimeSeriesSet contribution_time_series(numberofsourcesamplesets_);
3771
3772 // Set source names for time series
3773 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3774 {
3775 contribution_time_series.setname(source_idx, samplesetsorder_[source_idx]);
3776 }
3777
3778 // Perform bootstrap iterations
3779 for (size_t iteration = 0; iteration < num_iterations; iteration++)
3780 {
3781 // Create bootstrapped dataset with randomly excluded samples
3782 SourceSinkData bootstrapped_data = RandomlyEliminateSourceSamples(percentage);
3783 bootstrapped_data.InitializeParametersAndObservations(target_sample);
3784
3785 // Update progress if progress tracker available
3786 if (rtw_)
3787 {
3788 rtw_->SetProgress(double(iteration) / double(num_iterations));
3789 }
3790
3791 // Solve CMB model with appropriate transformation
3792 if (use_softmax)
3793 {
3795 }
3796 else
3797 {
3799 }
3800
3801 // Record contributions from this iteration
3802 contribution_time_series.append(iteration, bootstrapped_data.GetContributionVector().vec);
3803 }
3804
3805 return contribution_time_series;
3806}
3807
3809 Results* results,
3810 const double& percentage,
3811 unsigned int num_iterations,
3812 string target_sample,
3813 bool use_softmax)
3814{
3815 // Initialize contribution time series
3817
3818 // Set source names
3819 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3820 {
3821 contributions->setname(source_idx, samplesetsorder_[source_idx]);
3822 }
3823
3824 // Perform bootstrap iterations
3825 for (size_t iteration = 0; iteration < num_iterations; iteration++)
3826 {
3827 // Create bootstrapped dataset
3828 SourceSinkData bootstrapped_data = RandomlyEliminateSourceSamples(percentage);
3829 bootstrapped_data.InitializeParametersAndObservations(target_sample);
3830
3831 // Update progress
3832 if (rtw_)
3833 {
3834 rtw_->SetProgress(double(iteration) / double(num_iterations));
3835 }
3836
3837 // Solve CMB model
3838 if (use_softmax)
3839 {
3841 }
3842 else
3843 {
3845 }
3846
3847 // Record contributions
3848 contributions->append(iteration, bootstrapped_data.GetContributionVector().vec);
3849 }
3850
3851 // ========== Result 1: Contribution Time Series ==========
3852 ResultItem contributions_result;
3853 results->SetName("Error Analysis for target sample '" + target_sample + "'");
3854 contributions_result.SetName("Error Analysis");
3855 contributions_result.SetResult(contributions);
3856 contributions_result.SetType(result_type::stacked_bar_chart);
3857 contributions_result.SetShowAsString(false);
3858 contributions_result.SetShowTable(true);
3859
3860 // Only show graph for small sample sizes (to avoid clutter)
3861 if (num_iterations < 101)
3862 {
3863 contributions_result.SetShowGraph(true);
3864 }
3865 else
3866 {
3867 contributions_result.SetShowGraph(false);
3868 }
3869
3870 contributions_result.SetYLimit(_range::high, 1);
3871 contributions_result.SetYLimit(_range::low, 0);
3872 contributions_result.SetXAxisMode(xaxis_mode::counter);
3873 contributions_result.setYAxisTitle("Contribution");
3874 contributions_result.setXAxisTitle("Sample");
3875 results->Append(contributions_result);
3876
3877 // ========== Result 2: Posterior Distributions ==========
3878 CMBTimeSeriesSet* distributions = new CMBTimeSeriesSet();
3879 *distributions = contributions->distribution(100, 0, 0);
3880
3881 ResultItem distributions_result;
3882 distributions_result.SetName("Posterior Distributions");
3883 distributions_result.SetShowAsString(false);
3884 distributions_result.SetShowTable(true);
3885 distributions_result.SetType(result_type::distribution);
3886 distributions_result.SetResult(distributions);
3887 results->Append(distributions_result);
3888
3889 // ========== Result 3: 95% Credible Intervals ==========
3890 RangeSet* credible_intervals = new RangeSet();
3891
3892 for (size_t i = 0; i < GetSourceOrder().size(); i++)
3893 {
3894 Range interval;
3895
3896 // Compute statistics for this source
3897 double percentile_2_5 = contributions->at(i).percentile(0.025);
3898 double percentile_97_5 = contributions->at(i).percentile(0.975);
3899 double mean_contribution = contributions->at(i).mean();
3900 double median_contribution = contributions->at(i).percentile(0.5);
3901
3902 // Configure credible interval
3903 interval.Set(_range::low, percentile_2_5);
3904 interval.Set(_range::high, percentile_97_5);
3905 interval.SetMean(mean_contribution);
3906 interval.SetMedian(median_contribution);
3907
3908 (*credible_intervals)[contributions->getSeriesName(i)] = interval;
3909 }
3910
3911 ResultItem intervals_result;
3912 intervals_result.SetName("Source Contribution Credible Intervals");
3913 intervals_result.SetShowAsString(true);
3914 intervals_result.SetShowTable(true);
3915 intervals_result.SetType(result_type::rangeset);
3916 intervals_result.SetResult(credible_intervals);
3917 intervals_result.SetYAxisMode(yaxis_mode::normal);
3918 intervals_result.SetYLimit(_range::high, 1.0);
3919 intervals_result.SetYLimit(_range::low, 0);
3920 results->Append(intervals_result);
3921
3922 return true;
3923}
3924
3926 const string& source_group,
3927 bool use_softmax,
3928 bool apply_om_size_correction)
3929{
3930 // Initialize with first target sample (will be replaced in loop)
3932
3933 // Initialize result time series
3934 CMBTimeSeriesSet validation_results(numberofsourcesamplesets_);
3935
3936 // Set source names
3937 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
3938 {
3939 validation_results.setname(source_idx, samplesetsorder_[source_idx]);
3940 }
3941
3942 // Perform leave-one-out validation for each sample in the source group
3943 size_t valid_sample_count = 0;
3944 for (map<string, Elemental_Profile>::iterator sample = at(source_group).begin();
3945 sample != at(source_group).end();
3946 sample++)
3947 {
3948 // Create dataset with this sample as target
3949 SourceSinkData test_dataset = ReplaceSourceAsTarget(sample->first);
3950
3951 // Apply corrections if requested
3952 SourceSinkData corrected_dataset = test_dataset.CreateCorrectedDataset(
3953 sample->first, apply_om_size_correction, test_dataset.GetElementInformation());
3954 corrected_dataset.SetProgressReporter(rtw_);
3955
3956 // Check for negative values (would cause problems in CMB)
3957 vector<string> negative_elements = corrected_dataset.NegativeValueCheck();
3958
3959 if (negative_elements.size() == 0)
3960 {
3961 // No negative values - proceed with CMB solution
3962 test_dataset.InitializeParametersAndObservations(sample->first);
3963
3964 // Update progress
3965 if (rtw_)
3966 {
3967 rtw_->SetProgress(double(valid_sample_count) / double(at(source_group).size()));
3968 }
3969
3970 // Solve CMB model
3971 if (use_softmax)
3972 {
3974 }
3975 else
3976 {
3978 }
3979
3980 // Record contributions and label with sample name
3981 validation_results.append(valid_sample_count, test_dataset.GetContributionVector().vec);
3982 validation_results.SetLabel(valid_sample_count, sample->first);
3983 valid_sample_count++;
3984 }
3985 else
3986 {
3987 // Negative values detected - skip this sample and report
3988 for (size_t i = 0; i < negative_elements.size(); i++)
3989 {
3990 qDebug() << QString::fromStdString(negative_elements[i]);
3991 }
3992 }
3993 }
3994
3995 return validation_results;
3996}
3997
3999 transformation transform,
4000 bool apply_om_size_correction,
4001 map<string, vector<string>>& negative_elements)
4002{
4003 // Initialize with first target sample (will be replaced in loop)
4005
4006 // Initialize result time series
4007 CMBTimeSeriesSet contribution_results(numberofsourcesamplesets_);
4008
4009 // Set source names
4010 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
4011 {
4012 contribution_results.setname(source_idx, samplesetsorder_[source_idx]);
4013 }
4014
4015 // Solve CMB for each target sample
4016 size_t valid_sample_count = 0;
4017 for (map<string, Elemental_Profile>::iterator sample = at(target_group_).begin();
4018 sample != at(target_group_).end();
4019 sample++)
4020 {
4021 // Skip samples with empty names
4022 if (sample->first != "")
4023 {
4024 // Apply corrections if requested
4025 SourceSinkData corrected_dataset = CreateCorrectedDataset(
4026 sample->first, apply_om_size_correction, GetElementInformation());
4027
4028 // Check for negative values
4029 negative_elements[sample->first] = corrected_dataset.NegativeValueCheck();
4030
4031 if (negative_elements[sample->first].size() == 0)
4032 {
4033 // No negative values - proceed with CMB solution
4034 corrected_dataset.InitializeParametersAndObservations(sample->first);
4035
4036 // Update progress with sample name
4037 if (rtw_)
4038 {
4039 rtw_->SetProgress(double(valid_sample_count) / double(at(target_group_).size()));
4040 rtw_->SetLabel(QString::fromStdString(sample->first));
4041 }
4042
4043 // Solve CMB model
4044 corrected_dataset.SolveLevenberg_Marquardt(transform);
4045
4046 // Record contributions and label with sample name
4047 contribution_results.append(valid_sample_count, corrected_dataset.GetContributionVector().vec);
4048 contribution_results.SetLabel(valid_sample_count, sample->first);
4049 valid_sample_count++;
4050 }
4051 }
4052 }
4053
4054 // Set progress to 100% at completion
4055 if (rtw_)
4056 {
4057 rtw_->SetProgress(1.0);
4058 }
4059
4060 return contribution_results;
4061}
4062
4063
4065{
4066 vector<string> all_sample_names;
4067
4068 // Collect sample names from all source groups
4069 for (map<string, Elemental_Profile_Set>::const_iterator profile_set = cbegin();
4070 profile_set != cend();
4071 profile_set++)
4072 {
4073 // Skip target group
4074 if (profile_set->first != target_group_)
4075 {
4076 // Add all sample names from this source group
4077 for (map<string, Elemental_Profile>::const_iterator profile = profile_set->second.cbegin();
4078 profile != profile_set->second.cend();
4079 profile++)
4080 {
4081 all_sample_names.push_back(profile->first);
4082 }
4083 }
4084 }
4085
4086 return all_sample_names;
4087}
4088
4089vector<string> SourceSinkData::RandomlypickSamples(const double& percentage) const
4090{
4091 // Get all source sample names
4092 vector<string> all_samples = AllSourceSampleNames();
4093 vector<string> selected_samples;
4094
4095 // Perform Bernoulli sampling: each sample included independently with probability = percentage
4096 for (size_t i = 0; i < all_samples.size(); i++)
4097 {
4098 // Generate random number in [0, 1)
4099 double random_value = GADistribution::GetRndUniF(0, 1);
4100
4101 // Include sample if random value < percentage
4102 if (random_value < percentage)
4103 {
4104 selected_samples.push_back(all_samples[i]);
4105 }
4106 }
4107
4108 return selected_samples;
4109}
4110
4112 const string& target_sample,
4113 map<string, string> arguments,
4115 ProgressReporter* progress_window,
4116 const string& working_folder)
4117{
4118 // A null reporter stands in for a missing one so that the progress calls
4119 // below need no null test. Callers that want no progress display pass
4120 // nullptr.
4121 NullProgressReporter discarded_progress;
4122 if (progress_window == nullptr)
4123 progress_window = &discarded_progress;
4124
4125 // Initialize results container
4126 Results results;
4127 results.SetName("MCMC results for '" + target_sample + "'");
4128
4129 // Parse OM/size correction setting
4130 bool apply_om_size_correction = (arguments["Apply size and organic matter correction"] == "true");
4131
4132 // Apply corrections and validate
4133 SourceSinkData corrected_data = CreateCorrectedDataset(
4134 target_sample, apply_om_size_correction, GetElementInformation());
4135 vector<string> negative_elements = corrected_data.NegativeValueCheck();
4136
4137 if (negative_elements.size() > 0)
4138 {
4139 // Negative values detected - return error
4140 results.SetError("Negative elemental content in ");
4141 for (size_t i = 0; i < negative_elements.size(); i++)
4142 {
4143 if (i == 0)
4144 results.AppendError(negative_elements[i]);
4145 else
4146 results.AppendError("," + negative_elements[i]);
4147 }
4148 return results;
4149 }
4150
4151 // Configure MCMC sampler
4152 mcmc->Model = &corrected_data;
4153
4154 // Setup progress window charts
4155 progress_window->SetTitle("Acceptance Rate", 0);
4156 progress_window->SetTitle("Purturbation Factor", 1);
4157 progress_window->SetTitle("Log posterior value", 2);
4158 progress_window->SetYAxisTitle("Acceptance Rate", 0);
4159 progress_window->SetYAxisTitle("Purturbation Factor", 1);
4160 progress_window->SetYAxisTitle("Log posterior value", 2);
4161 progress_window->Start();
4162
4163 // ========== Run MCMC Sampling ==========
4164 corrected_data.InitializeParametersAndObservations(target_sample);
4165
4166 // Set MCMC parameters
4167 mcmc->SetProperty("number_of_samples", arguments["Number of samples"]);
4168 mcmc->SetProperty("number_of_chains", arguments["Number of chains"]);
4169 mcmc->SetProperty("number_of_burnout_samples", arguments["Samples to be discarded (burnout)"]);
4170 mcmc->SetProperty("dissolve_chains", arguments["Dissolve Chains"]);
4171
4172 // Initialize MCMC sampler
4174 mcmc->initialize(mcmc_samples, true);
4175
4176 // Determine output file path
4177 string output_path;
4178 if (!QString::fromStdString(arguments["Samples File Name"]).contains("/"))
4179 {
4180 output_path = working_folder + "/";
4181 }
4182
4183 // Run MCMC chains
4184 int num_chains = QString::fromStdString(arguments["Number of chains"]).toInt();
4185 int num_samples = QString::fromStdString(arguments["Number of samples"]).toInt();
4186 mcmc->step(num_chains, num_samples, output_path + arguments["Samples File Name"],
4187 mcmc_samples, progress_window);
4188
4189 // Append last contribution (computed from sum constraint)
4190 vector<string> source_names = corrected_data.SourceGroupNames();
4191 size_t last_source_idx = source_names.size() - 1;
4192 mcmc_samples->AppendLastContribution(last_source_idx,
4193 source_names[last_source_idx] + "_contribution");
4194
4195 // Store MCMC samples result
4196 ResultItem mcmc_samples_result;
4197 mcmc_samples_result.SetShowAsString(false);
4198 mcmc_samples_result.SetType(result_type::mcmc_samples);
4199 mcmc_samples_result.SetName("MCMC samples");
4200 mcmc_samples_result.SetResult(mcmc_samples);
4201 results.Append(mcmc_samples_result);
4202
4203 // Parse burnin parameter
4204 int burnin_samples = QString::fromStdString(arguments["Samples to be discarded (burnout)"]).toInt();
4205
4206 // ========== Result 1: Posterior Distributions for Contributions ==========
4207 ResultItem posterior_distributions_result;
4208 CMBTimeSeriesSet* posterior_distributions = new CMBTimeSeriesSet();
4209 *posterior_distributions = mcmc_samples->distribution(100, burnin_samples);
4210
4211 posterior_distributions_result.SetName("Posterior Distributions");
4212 posterior_distributions_result.SetShowAsString(false);
4213 posterior_distributions_result.SetShowTable(true);
4214 posterior_distributions_result.SetType(result_type::distribution);
4215 posterior_distributions_result.SetResult(posterior_distributions);
4216 results.Append(posterior_distributions_result);
4217
4218 // ========== Result 2: Contribution Credible Intervals ==========
4219 RangeSet* contribution_credible_intervals = new RangeSet();
4220
4221 for (size_t i = 0; i < corrected_data.GetSourceOrder().size(); i++)
4222 {
4223 Range interval;
4224
4225 // Compute statistics (excluding burnin)
4226 double percentile_2_5 = mcmc_samples->at(i).percentile(0.025, burnin_samples);
4227 double percentile_97_5 = mcmc_samples->at(i).percentile(0.975, burnin_samples);
4228 double mean_contribution = mcmc_samples->at(i).mean(burnin_samples);
4229 double median_contribution = mcmc_samples->at(i).percentile(0.5, burnin_samples);
4230
4231 interval.Set(_range::low, percentile_2_5);
4232 interval.Set(_range::high, percentile_97_5);
4233 interval.SetMean(mean_contribution);
4234 interval.SetMedian(median_contribution);
4235
4236 (*contribution_credible_intervals)[mcmc_samples->getSeriesName(i)] = interval;
4237 }
4238
4239 ResultItem contribution_intervals_result;
4240 contribution_intervals_result.SetName("Source Contribution Credible Intervals");
4241 contribution_intervals_result.SetShowAsString(true);
4242 contribution_intervals_result.SetShowTable(true);
4243 contribution_intervals_result.SetType(result_type::rangeset);
4244 contribution_intervals_result.SetResult(contribution_credible_intervals);
4245 contribution_intervals_result.SetYAxisMode(yaxis_mode::log);
4246 contribution_intervals_result.SetYLimit(_range::high, 1.0);
4247 results.Append(contribution_intervals_result);
4248
4249 // ========== Result 3: Predicted Element Distributions ==========
4250 CMBTimeSeriesSet predicted_samples = mcmc->predicted;
4251 vector<string> element_names = corrected_data.ElementOrder();
4252 vector<string> isotope_names = corrected_data.IsotopeOrder();
4253 vector<string> all_constituent_names = element_names;
4254 all_constituent_names.insert(all_constituent_names.end(), isotope_names.begin(), isotope_names.end());
4255
4256 // Set constituent names
4257 for (size_t i = 0; i < predicted_samples.size(); i++)
4258 {
4259 predicted_samples.setname(i, all_constituent_names[i]);
4260 }
4261
4262 // Extract element predictions only
4263 CMBTimeSeriesSet predicted_elements;
4264 for (size_t i = 0; i < predicted_samples.size(); i++)
4265 {
4266 if (corrected_data.GetElementInformation(predicted_samples.getSeriesName(i))->Role ==
4268 {
4269 predicted_elements.append(predicted_samples[i], predicted_samples.getSeriesName(i));
4270 }
4271 }
4272
4273 // Compute posterior distributions for elements
4274 CMBTimeSeriesSet* predicted_element_distributions = new CMBTimeSeriesSet();
4275 *predicted_element_distributions = predicted_elements.distribution(100, burnin_samples);
4276
4277 // Add observed values
4278 for (size_t i = 0; i < predicted_elements.size(); i++)
4279 {
4280 predicted_element_distributions->SetObservedValue(i, corrected_data.observation(i)->Value());
4281 }
4282
4283 ResultItem predicted_elements_result;
4284 predicted_elements_result.SetName("Posterior Predicted Constituents");
4285 predicted_elements_result.SetShowAsString(false);
4286 predicted_elements_result.SetShowTable(true);
4287 predicted_elements_result.SetType(result_type::distribution_with_observed);
4288 predicted_elements_result.SetResult(predicted_element_distributions);
4289 results.Append(predicted_elements_result);
4290
4291 // ========== Result 4: Element Credible Intervals ==========
4292 RangeSet* predicted_element_intervals = new RangeSet();
4293
4294 // Compute percentiles for all predictions
4295 vector<double> percentile_2_5_all = predicted_samples.percentile(0.025, burnin_samples);
4296 vector<double> percentile_97_5_all = predicted_samples.percentile(0.975, burnin_samples);
4297 vector<double> mean_all = predicted_samples.mean(burnin_samples);
4298 vector<double> median_all = predicted_samples.percentile(0.5, burnin_samples);
4299
4300 for (size_t i = 0; i < predicted_element_distributions->size(); i++)
4301 {
4302 Range interval;
4303 interval.Set(_range::low, percentile_2_5_all[i]);
4304 interval.Set(_range::high, percentile_97_5_all[i]);
4305 interval.SetMean(mean_all[i]);
4306 interval.SetMedian(median_all[i]);
4307
4308 (*predicted_element_intervals)[predicted_element_distributions->getSeriesName(i)] = interval;
4309 (*predicted_element_intervals)[predicted_element_distributions->getSeriesName(i)].SetValue(
4310 corrected_data.observation(i)->Value());
4311 }
4312
4313 ResultItem predicted_element_intervals_result;
4314 predicted_element_intervals_result.SetName("Predicted Samples Credible Intervals");
4315 predicted_element_intervals_result.SetShowAsString(true);
4316 predicted_element_intervals_result.SetShowTable(true);
4317 predicted_element_intervals_result.SetType(result_type::rangeset_with_observed);
4318 predicted_element_intervals_result.SetResult(predicted_element_intervals);
4319 predicted_element_intervals_result.SetYAxisMode(yaxis_mode::log);
4320 results.Append(predicted_element_intervals_result);
4321
4322 // ========== Result 5: Predicted Isotope Distributions ==========
4323 CMBTimeSeriesSet predicted_isotopes;
4324
4325 for (size_t i = 0; i < predicted_samples.size(); i++)
4326 {
4327 if (corrected_data.GetElementInformation(predicted_samples.getSeriesName(i))->Role ==
4329 {
4330 predicted_isotopes.append(predicted_samples[i], predicted_samples.getSeriesName(i));
4331 }
4332 }
4333
4334 CMBTimeSeriesSet* predicted_isotope_distributions = new CMBTimeSeriesSet();
4335 *predicted_isotope_distributions = predicted_isotopes.distribution(100, burnin_samples);
4336
4337 // Add observed values
4338 for (size_t i = 0; i < predicted_isotopes.size(); i++)
4339 {
4340 predicted_isotope_distributions->SetObservedValue(
4341 i, corrected_data.observation(i + element_names.size())->Value());
4342 }
4343
4344 ResultItem predicted_isotopes_result;
4345 predicted_isotopes_result.SetName("Posterior Predicted Isotopes");
4346 predicted_isotopes_result.SetShowAsString(false);
4347 predicted_isotopes_result.SetShowTable(true);
4348 predicted_isotopes_result.SetType(result_type::distribution_with_observed);
4349 predicted_isotopes_result.SetResult(predicted_isotope_distributions);
4350 results.Append(predicted_isotopes_result);
4351
4352 // ========== Result 6: Isotope Credible Intervals ==========
4353 RangeSet* predicted_isotope_intervals = new RangeSet();
4354 size_t element_offset = corrected_data.ElementOrder().size();
4355
4356 for (size_t i = 0; i < predicted_isotope_distributions->size(); i++)
4357 {
4358 Range interval;
4359 interval.Set(_range::low, percentile_2_5_all[i + element_offset]);
4360 interval.Set(_range::high, percentile_97_5_all[i + element_offset]);
4361 interval.SetMean(mean_all[i + element_offset]);
4362 interval.SetMedian(median_all[i + element_offset]);
4363
4364 (*predicted_isotope_intervals)[predicted_isotope_distributions->getSeriesName(i)] = interval;
4365 (*predicted_isotope_intervals)[predicted_isotope_distributions->getSeriesName(i)].SetValue(
4366 corrected_data.observation(i + element_offset)->Value());
4367 }
4368
4369 ResultItem predicted_isotope_intervals_result;
4370 predicted_isotope_intervals_result.SetName("Predicted Samples Credible Intervals for Isotopes");
4371 predicted_isotope_intervals_result.SetShowAsString(true);
4372 predicted_isotope_intervals_result.SetShowTable(true);
4373 predicted_isotope_intervals_result.SetType(result_type::rangeset_with_observed);
4374 predicted_isotope_intervals_result.SetResult(predicted_isotope_intervals);
4375 predicted_isotope_intervals_result.SetYAxisMode(yaxis_mode::normal);
4376 results.Append(predicted_isotope_intervals_result);
4377
4378 // Set progress to 100%
4379 progress_window->SetProgress(1.0);
4380
4381 return results;
4382}
4383
4384
4385// Replaces the characters Windows does not allow in a file or folder name, and
4386// the trailing dots and spaces it silently strips, so that the same name works
4387// on every platform.
4388static QString SafeFileName(const QString& name)
4389{
4390 QString safe = name;
4391 for (QChar& c : safe)
4392 {
4393 if (c.unicode() < 32 || QStringLiteral("\\/:*?\"<>|").contains(c))
4394 c = QLatin1Char('_');
4395 }
4396 while (safe.endsWith(QLatin1Char('.')) || safe.endsWith(QLatin1Char(' ')))
4397 safe.chop(1);
4398 return safe.isEmpty() ? QStringLiteral("_") : safe;
4399}
4400
4402 map<string, string> arguments,
4404 ProgressReporter* progress_window,
4405 const string& working_folder,
4406 vector<string>* failed_writes)
4407{
4408 // A null reporter stands in for a missing one so that the progress calls
4409 // below need no null test. Callers that want no progress display pass
4410 // nullptr.
4411 NullProgressReporter discarded_progress;
4412 if (progress_window == nullptr)
4413 progress_window = &discarded_progress;
4414
4415 // Initialize with first target sample
4417
4418 // Initialize contribution matrix: 4 columns per source (low, high, median, mean)
4419 const size_t num_target_samples = at(target_group_).size();
4420 const size_t num_columns = numberofsourcesamplesets_ * 4;
4421 CMBMatrix contribution_statistics(num_columns, num_target_samples);
4422
4423 // Set column labels
4424 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
4425 {
4426 const string& source_name = samplesetsorder_[source_idx];
4427 contribution_statistics.SetColumnLabel(source_idx * 4, source_name + "-low");
4428 contribution_statistics.SetColumnLabel(source_idx * 4 + 1, source_name + "-high");
4429 contribution_statistics.SetColumnLabel(source_idx * 4 + 2, source_name + "-median");
4430 contribution_statistics.SetColumnLabel(source_idx * 4 + 3, source_name + "-mean");
4431 }
4432
4433 // Process each target sample
4434 size_t sample_counter = 0;
4435 for (map<string, Elemental_Profile>::iterator sample = at(target_group_).begin();
4436 sample != at(target_group_).end();
4437 sample++)
4438 {
4439 const string& sample_name = sample->first;
4440
4441 // Create output directory for this sample
4442 QDir sample_dir(QString::fromStdString(working_folder) + "/" +
4443 SafeFileName(QString::fromStdString(sample_name)));
4444 const bool sample_dir_ok = sample_dir.exists() || sample_dir.mkpath(".");
4445 if (!sample_dir_ok && failed_writes != nullptr)
4446 {
4447 failed_writes->push_back("could not create folder " +
4448 sample_dir.absolutePath().toStdString());
4449 }
4450
4451 // Set matrix row label
4452 contribution_statistics.SetRowLabel(sample_counter, sample_name);
4453
4454 // Update progress label
4455 progress_window->SetLabel(QString::fromStdString(sample_name));
4456
4457 // Run MCMC analysis for this sample
4458 // The raw chain log goes in the sample's own folder, so that each sample
4459 // keeps its log instead of the next sample truncating a shared one. If
4460 // that folder could not be created, the log falls back to the parent.
4461 const string sample_folder = sample_dir_ok ? sample_dir.absolutePath().toStdString() : working_folder;
4462 Results mcmc_results = MCMC(sample_name, arguments, mcmc, progress_window, sample_folder);
4463
4464 // Save all result items to text files. The result keys have the form
4465 // "<index>:<name>", and a colon in a path on Windows names an NTFS
4466 // alternate data stream, so the key must be made safe before use.
4467 for (map<string, ResultItem>::iterator result_item = mcmc_results.begin();
4468 sample_dir_ok && result_item != mcmc_results.end();
4469 result_item++)
4470 {
4471 QString file_path = sample_dir.absolutePath() + "/" +
4472 SafeFileName(QString::fromStdString(result_item->first)) + ".txt";
4473 QFile output_file(file_path);
4474 if (!output_file.open(QIODevice::WriteOnly | QIODevice::Text))
4475 {
4476 if (failed_writes != nullptr)
4477 failed_writes->push_back(file_path.toStdString() + ": " +
4478 output_file.errorString().toStdString());
4479 continue;
4480 }
4481 result_item->second.Result()->writetofile(&output_file);
4482 output_file.close();
4483 if (output_file.error() != QFileDevice::NoError && failed_writes != nullptr)
4484 {
4485 failed_writes->push_back(file_path.toStdString() + ": " +
4486 output_file.errorString().toStdString());
4487 }
4488 }
4489
4490 // Extract contribution statistics from credible intervals
4491 RangeSet* credible_intervals = static_cast<RangeSet*>(
4492 mcmc_results.at("3:Source Contribution Credible Intervals").Result());
4493
4494 for (size_t source_idx = 0; source_idx < numberofsourcesamplesets_; source_idx++)
4495 {
4496 const string contribution_name = samplesetsorder_[source_idx] + "_contribution";
4497 const Range& contribution_interval = credible_intervals->at(contribution_name);
4498
4499 // Store statistics: low (2.5%), high (97.5%), median, mean
4500 contribution_statistics[sample_counter][4 * source_idx] = contribution_interval.Get(_range::low);
4501 contribution_statistics[sample_counter][4 * source_idx + 1] = contribution_interval.Get(_range::high);
4502 contribution_statistics[sample_counter][4 * source_idx + 2] = contribution_interval.Median();
4503 contribution_statistics[sample_counter][4 * source_idx + 3] = contribution_interval.Mean();
4504 }
4505
4506 sample_counter++;
4507
4508 // Update batch progress
4509 progress_window->SetProgress2(double(sample_counter) / double(num_target_samples));
4510
4511 // Clear MCMC progress charts for next sample
4512 progress_window->ClearGraph(0);
4513 progress_window->ClearGraph(1);
4514 progress_window->ClearGraph(2);
4515 }
4516
4517 // Set batch progress to 100%
4518 progress_window->SetProgress2(1.0);
4519
4520 return contribution_statistics;
4521}
4522bool SourceSinkData::ToolsUsed(const string& tool_name)
4523{
4524 // Search for tool in the tools_used list
4525 std::list<string>::iterator iter = std::find(tools_used_.begin(), tools_used_.end(), tool_name);
4526
4527 // Return true if found, false if not found
4528 return (iter != tools_used_.end());
4529}
4530
4532{
4533 // Search for first organic carbon constituent
4534 for (map<string, element_information>::iterator element = element_information_.begin();
4535 element != element_information_.end();
4536 element++)
4537 {
4538 if (element->second.Role == element_information::role::organic_carbon)
4539 {
4540 return element->first;
4541 }
4542 }
4543
4544 // No organic carbon constituent found
4545 return "";
4546}
4547
4549{
4550 // Search for first particle size constituent
4551 for (map<string, element_information>::iterator element = element_information_.begin();
4552 element != element_information_.end();
4553 element++)
4554 {
4555 if (element->second.Role == element_information::role::particle_size)
4556 {
4557 return element->first;
4558 }
4559 }
4560
4561 // No particle size constituent found
4562 return "";
4563}
4564
4566{
4567 vector<string> element_names = GetElementNames();
4568 CMatrix within_group_covariance(element_names.size());
4569
4570 // Accumulate weighted covariance matrices from each source group
4571 int total_degrees_of_freedom = 0;
4572 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4573 source_group != end();
4574 source_group++)
4575 {
4576 if (source_group->first != target_group_)
4577 {
4578 // Weight by degrees of freedom (n - 1) for this group
4579 int group_df = source_group->second.size() - 1;
4580 within_group_covariance += group_df * source_group->second.CalculateCovarianceMatrix();
4581 total_degrees_of_freedom += group_df;
4582 }
4583 }
4584
4585 // Return pooled within-group covariance
4586 return within_group_covariance / double(total_degrees_of_freedom);
4587}
4588
4590{
4591 vector<string> element_names = GetElementNames();
4592 CMatrix between_group_covariance(element_names.size());
4593
4594 // Get overall mean across all sources
4595 CMBVector overall_mean = MeanElementalContent();
4596
4597 // Accumulate weighted outer products of group mean deviations
4598 double total_samples = 0.0;
4599 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4600 source_group != end();
4601 source_group++)
4602 {
4603 if (source_group->first != target_group_)
4604 {
4605 // Compute deviation of group mean from overall mean
4606 CMBVector group_mean = MeanElementalContent(source_group->first);
4607 CMBVector deviation = overall_mean - group_mean;
4608
4609 // Add weighted outer product: n_i × (μ_i - μ)(μ_i - μ)ᵀ
4610 size_t group_size = source_group->second.size();
4611 for (size_t i = 0; i < element_names.size(); i++)
4612 {
4613 for (size_t j = 0; j < element_names.size(); j++)
4614 {
4615 between_group_covariance[i][j] += deviation[i] * deviation[j] * group_size;
4616 }
4617 }
4618
4619 total_samples += group_size;
4620 }
4621 }
4622
4623 // Return normalized between-group covariance
4624 return between_group_covariance / total_samples;
4625}
4626
4628{
4629 vector<string> element_names = GetElementNames();
4630 CMatrix total_scatter(element_names.size());
4631
4632 // Get overall mean across all sources
4633 CMBVector overall_mean = MeanElementalContent();
4634
4635 // Accumulate sum of squared deviations from overall mean
4636 double total_samples = 0.0;
4637 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4638 source_group != end();
4639 source_group++)
4640 {
4641 if (source_group->first != target_group_)
4642 {
4643 // Process each sample in this group
4644 for (map<string, Elemental_Profile>::iterator sample = source_group->second.begin();
4645 sample != source_group->second.end();
4646 sample++)
4647 {
4648 // Add outer product of deviation: (x - μ)(x - μ)ᵀ
4649 for (size_t i = 0; i < element_names.size(); i++)
4650 {
4651 for (size_t j = 0; j < element_names.size(); j++)
4652 {
4653 double deviation_i = overall_mean[i] - sample->second.at(element_names[i]);
4654 double deviation_j = overall_mean[j] - sample->second.at(element_names[j]);
4655 total_scatter[i][j] += deviation_i * deviation_j;
4656 }
4657 }
4658 }
4659
4660 total_samples += source_group->second.size();
4661 }
4662 }
4663
4664 // Return normalized total scatter matrix
4665 return total_scatter / total_samples;
4666}
4667
4669{
4670 // Get covariance matrices
4671 CMatrix_arma within_group_cov = WithinGroupCovarianceMatrix();
4672 CMatrix_arma between_group_cov = BetweenGroupCovarianceMatrix();
4673 CMatrix_arma total_cov = within_group_cov + between_group_cov;
4674
4675 // Compute Wilks' Lambda = |S_W| / |S_T|
4676 double numerator = within_group_cov.det();
4677 double denominator = total_cov.det();
4678
4679 // Use absolute values for numerical stability
4680 return fabs(numerator) / fabs(denominator);
4681}
4682
4684{
4685 int num_elements = GetElementNames().size();
4686
4687 // Get Wilks' Lambda (capped at 1.0)
4688 double wilks_lambda = min(WilksLambda(), 1.0);
4689
4690 // Transform to chi-squared statistic
4691 double sample_size = TotalNumberofSourceSamples();
4692 double correction_factor = (num_elements + (numberofsourcesamplesets_ - 1.0)) / 2.0;
4693 double chi_squared = -(sample_size - 1.0 - correction_factor) * log(wilks_lambda);
4694
4695 // Compute degrees of freedom
4696 double degrees_of_freedom;
4697 if (target_group_ != "")
4698 {
4699 // Target group present: exclude from group count
4700 degrees_of_freedom = num_elements * (numberofsourcesamplesets_ - 2.0);
4701 }
4702 else
4703 {
4704 // No target group
4705 degrees_of_freedom = num_elements * (numberofsourcesamplesets_ - 1.0);
4706 }
4707
4708 // Compute p-value from chi-squared distribution
4709 double p_value = gsl_cdf_chisq_Q(chi_squared, degrees_of_freedom);
4710
4711 return p_value;
4712}
4713
4715{
4716 // Get discriminant eigenvector
4717 CMBVector discriminant_function = DFA_eigvector();
4718
4719 CMBVectorSet projected_scores;
4720
4721 // Project each group onto discriminant axis
4722 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4723 source_group != end();
4724 source_group++)
4725 {
4726 // Compute discriminant scores for this group
4727 CMBVector group_scores = source_group->second.CalculateDotProduct(discriminant_function);
4728 projected_scores.Append(source_group->first, group_scores);
4729 }
4730
4731 return projected_scores;
4732}
4733
4734CMBVectorSet SourceSinkData::DFA_Projected(const string& source1, const string& source2)
4735{
4736 // Get discriminant eigenvector
4737 CMBVector discriminant_function = DFA_eigvector();
4738
4739 CMBVectorSet projected_scores;
4740
4741 // Project each group onto discriminant axis
4742 // Note: Parameters source1 and source2 not currently used
4743 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4744 source_group != end();
4745 source_group++)
4746 {
4747 // Compute discriminant scores for this group
4748 CMBVector group_scores = source_group->second.CalculateDotProduct(discriminant_function);
4749 projected_scores.Append(source_group->first, group_scores);
4750 }
4751
4752 return projected_scores;
4753}
4754
4756{
4757 // Get discriminant eigenvector from current (reduced) dataset
4758 CMBVector discriminant_function = DFA_eigvector();
4759
4760 CMBVectorSet projected_scores;
4761
4762 // Project samples from original dataset onto discriminant axis
4763 for (map<string, Elemental_Profile_Set>::iterator source_group = original->begin();
4764 source_group != original->end();
4765 source_group++)
4766 {
4767 // Skip target group
4768 if (source_group->first != original->target_group_)
4769 {
4770 // Compute discriminant scores for this group
4771 CMBVector group_scores = source_group->second.CalculateDotProduct(discriminant_function);
4772 projected_scores.Append(source_group->first, group_scores);
4773 }
4774 }
4775
4776 return projected_scores;
4777}
4778
4780{
4781 // Get covariance matrices
4782 CMatrix_arma between_group_cov = BetweenGroupCovarianceMatrix();
4783 CMatrix_arma within_group_cov = WithinGroupCovarianceMatrix();
4784
4785 // Invert within-group covariance
4786 CMatrix_arma inv_within_cov = inv(within_group_cov);
4787
4788 // Check if inversion failed (singular matrix)
4789 if (inv_within_cov.getnumrows() == 0)
4790 {
4791 return CMBVector(); // Return empty vector
4792 }
4793
4794 // Compute product matrix for eigenvalue problem
4795 CMatrix_arma product_matrix = inv_within_cov * between_group_cov;
4796
4797 // Solve eigenvalue problem
4798 arma::cx_vec eigenvalues;
4799 arma::cx_mat eigenvectors;
4800 eig_gen(eigenvalues, eigenvectors, product_matrix);
4801
4802 // Extract real parts
4803 CVector_arma real_eigenvalues = GetReal(eigenvalues);
4804 CMatrix_arma real_eigenvectors = GetReal(eigenvectors);
4805
4806 // Find eigenvector corresponding to largest eigenvalue (by absolute value)
4807 size_t max_eigenvalue_index = real_eigenvalues.abs_max_elems();
4808 CMBVector discriminant_function = CVector_arma(real_eigenvectors.getcol(max_eigenvalue_index));
4809
4810 // Label with element names
4811 vector<string> element_names = GetElementNames();
4812 discriminant_function.SetLabels(element_names);
4813
4814 return discriminant_function;
4815}
4816
4817CMBVector SourceSinkData::DFA_weight_vector(const string& source1, const string& source2)
4818{
4819 // Get covariance matrices for both groups
4820 CMatrix_arma cov_matrix1 = at(source1).CalculateCovarianceMatrix();
4821 CMatrix_arma cov_matrix2 = at(source2).CalculateCovarianceMatrix();
4822
4823 // Get mean vectors for both groups
4824 CVector mean_vector1 = at(source1).CalculateElementMeans();
4825 CVector mean_vector2 = at(source2).CalculateElementMeans();
4826 CVector_arma mean1 = mean_vector1;
4827 CVector_arma mean2 = mean_vector2;
4828
4829 // Compute Fisher's linear discriminant: w = (μ₂ - μ₁) / (Σ₁ + Σ₂)
4830 CMatrix_arma pooled_cov = cov_matrix1 + cov_matrix2;
4831 CVector weight_vector_arma = (mean2 - mean1) / pooled_cov;
4832
4833 // Convert to CMBVector and normalize
4834 CMBVector weight_vector = weight_vector_arma;
4835 weight_vector = weight_vector / weight_vector.norm2();
4836
4837 // Label with element names
4838 weight_vector.SetLabels(GetElementNames());
4839
4840 return weight_vector;
4841}
4842
4844{
4845 // Check if group exists
4846 if (count(group_name) == 0)
4847 {
4848 return CMBVector(); // Return empty vector
4849 }
4850
4851 // Compute mean concentrations for this group
4852 vector<string> element_names = GetElementNames();
4853 CMBVector group_mean = at(group_name).CalculateElementMeans();
4854 group_mean.SetLabels(element_names);
4855
4856 return group_mean;
4857}
4858
4860{
4861 vector<string> element_names = GetElementNames();
4862 CMBVector weighted_mean(element_names.size());
4863
4864 // Accumulate weighted means from each source group
4865 double total_samples = 0.0;
4866 for (map<string, Elemental_Profile_Set>::iterator source_group = begin();
4867 source_group != end();
4868 source_group++)
4869 {
4870 // Skip target group
4871 if (source_group->first != target_group_)
4872 {
4873 // Weight group mean by sample size
4874 size_t group_size = source_group->second.size();
4875 CMBVector group_mean = MeanElementalContent(source_group->first);
4876
4877 weighted_mean += double(group_size) * group_mean;
4878 total_samples += double(group_size);
4879 }
4880 }
4881
4882 // Set labels and normalize by total sample count
4883 weighted_mean.SetLabels(element_order_);
4884
4885 return weighted_mean / total_samples;
4886}
4887
4889 const string& source1,
4890 const string& source2)
4891{
4892 // Initialize result vectors: [0]=p-values, [1]=Wilks' Lambda, [2]=F-test p-values
4893 vector<CMBVector> stepwise_results(3);
4894
4895 vector<string> element_names = GetElementNames();
4896 vector<string> selected_elements;
4897
4898 // Forward selection: add one element at a time
4899 for (size_t step = 0; step < element_names.size(); step++)
4900 {
4901 // Update progress
4902 if (rtw_)
4903 {
4904 rtw_->SetProgress(double(step + 1) / double(element_names.size()));
4905 }
4906
4907 // Find best element to add next
4908 double best_p_value = 100.0;
4909 string best_element;
4910 double best_wilks_lambda;
4911 double best_f_test_p_value;
4912
4913 // Test each unselected element
4914 for (size_t j = 0; j < element_names.size(); j++)
4915 {
4916 // Skip if already selected
4917 if (lookup(selected_elements, element_names[j]) == -1)
4918 {
4919 // Create candidate selection with this element added
4920 vector<string> candidate_elements = selected_elements;
4921 candidate_elements.push_back(element_names[j]);
4922
4923 // Extract dataset with only candidate elements
4924 SourceSinkData candidate_dataset = ExtractSpecificElements(candidate_elements);
4925
4926 // Perform DFA on candidate dataset
4927 DFA_result candidate_dfa = candidate_dataset.DiscriminantFunctionAnalysis(source1, source2);
4928
4929 // Check if DFA succeeded
4930 if (candidate_dfa.eigen_vectors.size() == 0)
4931 {
4932 return stepwise_results; // Return empty results on failure
4933 }
4934
4935 // Update best if this element improves discrimination
4936 if (candidate_dfa.p_values[0] < best_p_value)
4937 {
4938 best_element = element_names[j];
4939 best_p_value = candidate_dfa.p_values[0];
4940 best_wilks_lambda = candidate_dfa.wilkslambda[0];
4941 best_f_test_p_value = candidate_dfa.F_test_P_value[0];
4942 }
4943 }
4944 }
4945
4946 // Record statistics for best element at this step
4947 stepwise_results[0].append(best_element, best_p_value);
4948 stepwise_results[1].append(best_element, best_wilks_lambda);
4949 stepwise_results[2].append(best_element, best_f_test_p_value);
4950
4951 // Add best element to selection
4952 selected_elements.push_back(best_element);
4953 }
4954
4955 return stepwise_results;
4956}
4957
4959{
4960 // Initialize result vectors: [0]=p-values, [1]=Wilks' Lambda, [2]=F-test p-values
4961 vector<CMBVector> stepwise_results(3);
4962
4963 vector<string> element_names = GetElementNames();
4964 vector<string> selected_elements;
4965
4966 // Forward selection: add one element at a time
4967 for (size_t step = 0; step < element_names.size(); step++)
4968 {
4969 // Update progress
4970 if (rtw_)
4971 {
4972 rtw_->SetProgress(double(step + 1) / double(element_names.size()));
4973 }
4974
4975 // Find best element to add next
4976 double best_p_value = 100.0;
4977 string best_element;
4978 double best_wilks_lambda;
4979 double best_f_test_p_value;
4980
4981 // Test each unselected element
4982 for (size_t j = 0; j < element_names.size(); j++)
4983 {
4984 // Skip if already selected
4985 if (lookup(selected_elements, element_names[j]) == -1)
4986 {
4987 // Create candidate selection with this element added
4988 vector<string> candidate_elements = selected_elements;
4989 candidate_elements.push_back(element_names[j]);
4990
4991 // Extract dataset with only candidate elements
4992 SourceSinkData candidate_dataset = ExtractSpecificElements(candidate_elements);
4993
4994 // Compute multi-group DFA statistics
4995 double p_value = candidate_dataset.DFA_P_Value();
4996
4997 // Update best if this element improves discrimination
4998 if (p_value < best_p_value)
4999 {
5000 best_element = element_names[j];
5001 best_p_value = p_value;
5002 best_wilks_lambda = candidate_dataset.WilksLambda();
5003
5004 // Compute F-test p-value from projections
5005 CMBVectorSet projected = candidate_dataset.DFA_Projected();
5006 best_f_test_p_value = projected.FTest_p_value();
5007 }
5008 }
5009 }
5010
5011 // Record statistics for best element at this step
5012 stepwise_results[0].append(best_element, best_p_value);
5013 stepwise_results[1].append(best_element, best_wilks_lambda);
5014 stepwise_results[2].append(best_element, best_f_test_p_value);
5015
5016 // Add best element to selection
5017 selected_elements.push_back(best_element);
5018 }
5019
5020 return stepwise_results;
5021}
5022
5023vector<CMBVector> SourceSinkData::StepwiseDiscriminantFunctionAnalysis(const string& source1)
5024{
5025 // Initialize result vectors: [0]=p-values, [1]=Wilks' Lambda, [2]=F-test p-values
5026 vector<CMBVector> stepwise_results(3);
5027
5028 vector<string> element_names = GetElementNames();
5029 vector<string> selected_elements;
5030
5031 // Forward selection: add one element at a time
5032 for (size_t step = 0; step < element_names.size(); step++)
5033 {
5034 // Update progress
5035 if (rtw_)
5036 {
5037 rtw_->SetProgress(double(step + 1) / double(element_names.size()));
5038 }
5039
5040 // Find best element to add next
5041 double best_p_value = 100.0;
5042 string best_element;
5043 double best_wilks_lambda;
5044 double best_f_test_p_value;
5045
5046 // Test each unselected element
5047 for (size_t j = 0; j < element_names.size(); j++)
5048 {
5049 // Skip if already selected
5050 if (lookup(selected_elements, element_names[j]) == -1)
5051 {
5052 // Create candidate selection with this element added
5053 vector<string> candidate_elements = selected_elements;
5054 candidate_elements.push_back(element_names[j]);
5055
5056 // Extract dataset with only candidate elements
5057 SourceSinkData candidate_dataset = ExtractSpecificElements(candidate_elements);
5058
5059 // Perform one-vs-rest DFA on candidate dataset
5060 DFA_result candidate_dfa = candidate_dataset.DiscriminantFunctionAnalysis(source1);
5061
5062 // Update best if this element improves discrimination
5063 if (candidate_dfa.p_values[0] < best_p_value)
5064 {
5065 best_element = element_names[j];
5066 best_p_value = candidate_dfa.p_values[0];
5067 best_wilks_lambda = candidate_dfa.wilkslambda[0];
5068 best_f_test_p_value = candidate_dfa.F_test_P_value[0];
5069 }
5070 }
5071 }
5072
5073 // Record statistics for best element at this step
5074 stepwise_results[0].append(best_element, best_p_value);
5075 stepwise_results[1].append(best_element, best_wilks_lambda);
5076 stepwise_results[2].append(best_element, best_f_test_p_value);
5077
5078 // Add best element to selection
5079 selected_elements.push_back(best_element);
5080 }
5081
5082 return stepwise_results;
5083}
5084
5085// ========================================
5086// METHOD DEFINITIONS TO MOVE TO sourcesinkdata.cpp
5087// ========================================
5088// These were previously inline in the header and should now be implemented in the .cpp file
5089
5090// ========== Accessors ==========
5091
5092vector<Parameter>& SourceSinkData::Parameters()
5093{
5094 return parameters_;
5095}
5096
5098{
5099 return parameters_.size();
5100}
5101
5103{
5104 return observations_.size();
5105}
5106
5108{
5109 if (i >= 0 && i < parameters_.size())
5110 return &parameters_[i];
5111 else
5112 return nullptr;
5113}
5114
5116{
5117 if (i >= 0 && i < parameters_.size())
5118 return &parameters_[i];
5119 else
5120 return nullptr;
5121}
5122
5124{
5125 if (i >= 0 && i < observations_.size())
5126 return &observations_[i];
5127 else
5128 return nullptr;
5129}
5130
5131bool SourceSinkData::SetTargetGroup(const string& targroup)
5132{
5133 target_group_ = targroup;
5134 return true;
5135}
5136
5138{
5139 return target_group_;
5140}
5141
5146
5151
5153{
5154 rtw_ = _rtw;
5155}
5156
5157map<string, element_information>* SourceSinkData::GetElementInformation()
5158{
5159 return &element_information_;
5160}
5161
5163{
5164 if (element_information_.count(element_name))
5165 return &element_information_.at(element_name);
5166 else
5167 return nullptr;
5168}
5169
5171{
5172 if (element_distributions_.count(element_name))
5173 return &element_distributions_.at(element_name);
5174 else
5175 return nullptr;
5176}
5177
5178ConcentrationSet* SourceSinkData::GetElementDistribution(const string& element_name, const string& sample_group)
5179{
5180 if (!GetSampleSet(sample_group))
5181 {
5182 cout << "Sample Group '" + sample_group + "' does not exist!" << std::endl;
5183 return nullptr;
5184 }
5185 if (!GetSampleSet(sample_group)->GetElementDistribution(element_name))
5186 {
5187 cout << "Element '" + element_name + "' does not exist!" << std::endl;
5188 return nullptr;
5189 }
5190 return GetSampleSet(sample_group)->GetElementDistribution(element_name);
5191}
5192
5193// ========== Ordering Accessors ==========
5194
5196{
5197 return samplesetsorder_;
5198}
5199
5201{
5202 return samplesetsorder_;
5203}
5204
5206{
5207 return constituent_order_;
5208}
5209
5211{
5212 return element_order_;
5213}
5214
5216{
5217 return isotope_order_;
5218}
5219
5221{
5222 return size_om_order_;
5223}
5224
5225// ========== OM/Size Constituent Management ==========
5226
5227void SourceSinkData::SetOMandSizeConstituents(const string& _omconstituent, const string& _sizeconsituent)
5228{
5229 omconstituent_ = _omconstituent;
5230 sizeconsituent_ = _sizeconsituent;
5231}
5232
5233void SourceSinkData::SetOMandSizeConstituents(const vector<string>& _omsizeconstituents)
5234{
5235 if (_omsizeconstituents.size() == 0)
5236 return;
5237 else if (_omsizeconstituents.size() == 1)
5238 omconstituent_ = _omsizeconstituents[0];
5239 else if (_omsizeconstituents.size() == 2)
5240 {
5241 omconstituent_ = _omsizeconstituents[0];
5242 sizeconsituent_ = _omsizeconstituents[1];
5243 }
5244}
5245
5247{
5248 vector<string> out;
5249 out.push_back(omconstituent_);
5250 out.push_back(sizeconsituent_);
5251 return out;
5252}
5253
5254// ========== Options Management ==========
5255
5256QMap<QString, double>* SourceSinkData::GetOptions()
5257{
5258 return &options_;
5259}
5260
5261// ========== Output Path Management ==========
5262
5264{
5265 return outputpath_;
5266}
5267
5268bool SourceSinkData::SetOutputPath(const string& output_path)
5269{
5270 outputpath_ = output_path;
5271 return true;
5272}
5273
5274// ========== Selected Target Sample Management ==========
5275
5276bool SourceSinkData::SetSelectedTargetSample(const string& sample_name)
5277{
5278 // Validate that sample exists in target group
5279 if (count(target_group_) == 0)
5280 return false;
5281
5282 if (at(target_group_).count(sample_name) == 0)
5283 return false;
5284
5285 selected_target_sample_ = sample_name;
5286 return true;
5287}
5288
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:71
bool step(int k, int chain_counter)
Perform single MCMC step for one chain.
Definition MCMC.hpp:340
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:203
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.
void SetType(const distribution_type &typ)
Set the distribution type and resize parameters vector.
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)
ProgressReporter that discards everything reported to it.
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
Abstract sink for progress information produced by a running analysis.
virtual void SetLabel(const QString &label)=0
Sets the free-text status label.
virtual void SetProgress2(const double &prog)=0
Sets the secondary progress fraction, where one is displayed.
virtual void Start()
Makes the reporter visible or otherwise begins reporting.
virtual void SetXRange(const double &x0, const double &x1, int chart=0)=0
Sets the horizontal range of a chart.
virtual void SetYAxisTitle(const QString &title, int chart=0)=0
Sets the vertical axis title of a chart.
virtual void SetProgress(const double &prog)=0
Sets the primary progress fraction.
virtual void ClearGraph(int chart=0)=0
Discards the points accumulated in a chart.
virtual void SetTitle(const QString &title, int chart=0)=0
Sets the title of a chart.
virtual void AppendPoint(const double &x, const double &y, int chart=0)=0
Adds a point to one of the progress charts.
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.
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.
CMBMatrix MCMC_Batch(map< string, string > arguments, CMCMC< SourceSinkData > *mcmc, ProgressReporter *progress_window, const string &working_folder, vector< string > *failed_writes=nullptr)
Performs batch MCMC analysis on all target samples.
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_
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.
ProgressReporter * rtw_
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.
Results MCMC(const string &target_sample, map< string, string > arguments, CMCMC< SourceSinkData > *mcmc, ProgressReporter *progress_window, const string &working_folder)
Performs Markov Chain Monte Carlo analysis for Bayesian source apportionment.
ResultItem GetCalculatedElementMeans()
Computes estimated mean concentrations for all source elements.
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.
void SetProgressReporter(ProgressReporter *_rtw)
Sets the progress window for displaying optimization progress.
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
static QString SafeFileName(const QString &name)
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