diff --git a/CITATION.cff b/CITATION.cff index cfc3e22..02f2fca 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -29,7 +29,7 @@ keywords: - risk mitigation - transparent reasoning -version: "2.5.0" +version: "3.0.0" license: non-commercial repository-code: https://github.com/dfeen87/CuraFrame url: https://github.com/dfeen87/CuraFrame @@ -40,6 +40,6 @@ preferred-citation: - given-names: Don Michael family-names: Feeney Jr. title: CuraFrame - version: "2.5.0" + version: "3.0.0" year: 2026 url: https://github.com/dfeen87/CuraFrame diff --git a/README.md b/README.md index 3fe8f71..7b95ab7 100644 --- a/README.md +++ b/README.md @@ -799,7 +799,7 @@ If CuraFrame is used in research, publications, or technical reports, please cit title = {CuraFrame: Constraint-Driven Therapeutic Design Reasoning}, author = {Feeney, Don Michael}, year = {2026}, - version = {2.5.0}, + version = {3.0.0}, url = {https://github.com/dfeen87/CuraFrame}, license = {Non-Commercial} } diff --git a/constraint_core/MultiBundleEvaluator.hpp b/constraint_core/MultiBundleEvaluator.hpp index 6a3fa41..515a6de 100644 --- a/constraint_core/MultiBundleEvaluator.hpp +++ b/constraint_core/MultiBundleEvaluator.hpp @@ -4,6 +4,8 @@ #include "Candidate.hpp" #include "EvaluationReport.hpp" #include "ConstraintRegistry.hpp" +#include "../scoring/ScoringPipeline.hpp" +#include "../scoring/WeightProfile.hpp" #include // Unified Evaluation Layer @@ -46,6 +48,18 @@ class MultiBundleEvaluator { report.combined_narrative = combined_narrative.str(); return report; } + + // Pass evaluation results to the scoring engine using the default profile + ScoringReport score(const EvaluationReport& report) const { + ScoringPipeline pipeline(std::make_shared()); + return pipeline.execute(report); + } + + // Pass evaluation results to the scoring engine using a specific profile + ScoringReport score_with_profile(const EvaluationReport& report, std::shared_ptr profile) const { + ScoringPipeline pipeline(profile); + return pipeline.execute(report); + } }; #endif // CURAFRAME_MULTI_BUNDLE_EVALUATOR_HPP diff --git a/cura_frame/__init__.py b/cura_frame/__init__.py index 13f91f8..44a010f 100644 --- a/cura_frame/__init__.py +++ b/cura_frame/__init__.py @@ -11,7 +11,7 @@ See docs/PHILOSOPHY.md and docs/ETHICAL_USE.md for guiding principles. """ -__version__ = "2.5.0" +__version__ = "3.0.0" from .core import ( CuraFrame, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e767ae3..1e9bb12 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to the C++ parallel evaluation universe will be documented in this file. +## [3.0.0] - Weighted Scoring Engine + +### Added +- **Weighted Multi-Constraint Scoring Engine**: Introduced a unified scoring engine that computes a Composite Stability Score (0-100) from constraint bundle outputs. +- **Scoring Pipeline & Reports**: Added `ScoringPipeline` and `ScoringReport` to aggregate penalties, bonuses, falsification impacts, and generate narrative summaries. +- **Weight Profiles**: Introduced configurable `WeightProfile` abstractions, including `DefaultResearchProfile` and `HighSafetyProfile`, for dynamic domain and signal weighting. +- **Integration**: Updated `MultiBundleEvaluator` to seamlessly pass evaluation reports into the scoring engine via `score` and `score_with_profile` methods without altering underlying constraint logic. +- **Documentation**: Added `scoring_engine.md` and `weight_profiles.md` detailing the scoring architecture and weight profile designs. + +### Changed +- Bumped project version to 3.0.0. + ## [2.5.0] - Constraint-Bundle Universe ### Added diff --git a/docs/scoring_engine.md b/docs/scoring_engine.md new file mode 100644 index 0000000..731b429 --- /dev/null +++ b/docs/scoring_engine.md @@ -0,0 +1,36 @@ +# CuraFrame Weighted Multi-Constraint Scoring Engine (v3.0.0) + +## Overview + +The Weighted Multi-Constraint Scoring Engine is a subsystem added in v3.0.0 that sits above the existing constraint bundles. It takes the output (signals, falsifications, and narrative summaries) from all bundles and distills them into a unified, modular, and configurable **Composite Stability Score**. + +This system does not alter or modify the underlying deterministic bundle evaluation logic. Instead, it aggregates results and applies domain-specific weighting, penalties, and bonuses to produce a nuanced report of therapeutic candidate viability. + +## Architecture + +1. **WeightProfile (`scoring/WeightProfile.hpp`)**: An interface (with default implementations) that defines the weights, penalties, multipliers, and scaling factors for different domains and specific signals. +2. **WeightedScoringEngine (`scoring/WeightedScoringEngine.hpp/.cpp`)**: The core engine that ingests the outputs from all bundles (`EvaluationReport`), applies the provided `WeightProfile`, computes the final score, and generates a structured summary. +3. **ScoringPipeline (`scoring/ScoringPipeline.hpp/.cpp`)**: A wrapper pipeline managing the execution of the scoring engine with a specified weight profile. +4. **ScoringReport (`scoring/ScoringReport.hpp`)**: The structured output structure, detailing penalty breakdowns, bonus breakdowns, falsification impacts, and a narrative summary. + +## Composite Stability Score + +The Composite Stability Score is a numerical representation of candidate stability scaled between 0 and 100: +- **100**: Perfect theoretical safety baseline, zero constraint violations or penalized signals. +- **< 100**: Progressive deterioration of the score based on domain-weighted penalties, amplified by specific severity levels, or falsification flags. +- **Falsification impact**: Candidate falsification triggers massive penalty deductions (configurable per weight profile), severely lowering the final score. + +Higher scores indicate more stable candidates, and lower scores indicate more risk-burdened candidates. + +## Example Report Execution + +```cpp +MultiBundleEvaluator evaluator; +EvaluationReport eval_report = evaluator.evaluate(candidate); + +// Generate standard composite scoring +ScoringReport score = evaluator.score(eval_report); + +// Generate with an aggressive safety profile +ScoringReport safety_score = evaluator.score_with_profile(eval_report, std::make_shared()); +``` diff --git a/docs/weight_profiles.md b/docs/weight_profiles.md new file mode 100644 index 0000000..3469a57 --- /dev/null +++ b/docs/weight_profiles.md @@ -0,0 +1,41 @@ +# Weight Profiles (v3.0.0) + +## Design and Intent + +Weight Profiles allow the CuraFrame Scoring Engine to view candidates under different lenses of scrutiny. Instead of hardcoded aggregation metrics, `WeightProfile` provides a configurable approach to amplifying certain domains (e.g., Cardiac or CNS safety) based on the context of the evaluation. + +## Built-in Profiles + +### 1. `DefaultResearchProfile` +- A balanced, general-purpose profile. +- Mildly emphasizes safety parameters but treats most domains with an equal base weight. + +### 2. `HighSafetyProfile` +- Designed for late-stage conservative screening. +- Applies heavy multipliers to `Safety`, `Cardiac`, `CNS`, and `SystemicExposure` domains. +- Heavily amplifies specific toxicity signals. +- Punishes falsification flags almost entirely zeroing out candidate viability. + +## Extending Weight Profiles + +To create a new weight profile, inherit from the `WeightProfile` abstract base class and implement the necessary configuration hooks. + +```cpp +class PediatricSafetyProfile : public WeightProfile { +public: + std::string name() const override { return "PediatricSafetyProfile"; } + + double bundle_weight(const std::string& bundle_name) const override { + // Significantly amplify systemic exposure and safety for pediatric focus + if (bundle_name == "SystemicExposure" || bundle_name == "Safety") return 3.0; + return 1.0; + } + + double signal_weight(const std::string& bundle_name, const std::string& signal_name) const override { + if (signal_name.find("clearance") != std::string::npos) return 2.0; + return 1.0; + } + + double falsification_penalty() const override { return 100.0; } +}; +``` diff --git a/scoring/ScoringPipeline.cpp b/scoring/ScoringPipeline.cpp new file mode 100644 index 0000000..d5339e2 --- /dev/null +++ b/scoring/ScoringPipeline.cpp @@ -0,0 +1,10 @@ +#include "ScoringPipeline.hpp" + +ScoringPipeline::ScoringPipeline(std::shared_ptr profile) + : engine_(std::move(profile)) {} + +ScoringReport ScoringPipeline::execute(const EvaluationReport& eval_report) const { + // In a more complex pipeline, normalization or pre-processing could happen here + // Currently, the WeightedScoringEngine handles everything deterministically + return engine_.score(eval_report); +} diff --git a/scoring/ScoringPipeline.hpp b/scoring/ScoringPipeline.hpp new file mode 100644 index 0000000..2ead0c5 --- /dev/null +++ b/scoring/ScoringPipeline.hpp @@ -0,0 +1,20 @@ +#ifndef CURAFRAME_SCORING_PIPELINE_HPP +#define CURAFRAME_SCORING_PIPELINE_HPP + +#include "../constraint_core/EvaluationReport.hpp" +#include "ScoringReport.hpp" +#include "WeightedScoringEngine.hpp" +#include "WeightProfile.hpp" +#include + +class ScoringPipeline { +public: + ScoringPipeline(std::shared_ptr profile); + + ScoringReport execute(const EvaluationReport& eval_report) const; + +private: + WeightedScoringEngine engine_; +}; + +#endif // CURAFRAME_SCORING_PIPELINE_HPP diff --git a/scoring/ScoringReport.hpp b/scoring/ScoringReport.hpp new file mode 100644 index 0000000..cd5430f --- /dev/null +++ b/scoring/ScoringReport.hpp @@ -0,0 +1,29 @@ +#ifndef CURAFRAME_SCORING_REPORT_HPP +#define CURAFRAME_SCORING_REPORT_HPP + +#include +#include +#include + +struct ScoringReport { + double composite_score = 0.0; // 0 to 100 + + // Per-bundle weighted contributions + std::map bundle_contributions; + + // Breakdowns + std::map penalty_breakdown; + std::map bonus_breakdown; + + // Falsification + std::vector falsification_flags; + double falsification_impact = 0.0; + + // Metadata + std::string weight_profile_name; + + // Narrative summary + std::string narrative_summary; +}; + +#endif // CURAFRAME_SCORING_REPORT_HPP diff --git a/scoring/WeightProfile.cpp b/scoring/WeightProfile.cpp new file mode 100644 index 0000000..5374b30 --- /dev/null +++ b/scoring/WeightProfile.cpp @@ -0,0 +1,4 @@ +#include "WeightProfile.hpp" + +// Intentionally left blank or simple implementations if needed. +// Weight profiles are currently fully implemented inline in the header for simplicity and polymorphism. diff --git a/scoring/WeightProfile.hpp b/scoring/WeightProfile.hpp new file mode 100644 index 0000000..e88a6c3 --- /dev/null +++ b/scoring/WeightProfile.hpp @@ -0,0 +1,71 @@ +#ifndef CURAFRAME_WEIGHT_PROFILE_HPP +#define CURAFRAME_WEIGHT_PROFILE_HPP + +#include +#include +#include + +class WeightProfile { +public: + virtual ~WeightProfile() = default; + + virtual std::string name() const = 0; + + // Default fallback weight if a bundle or signal is not explicitly defined + virtual double default_weight() const { return 1.0; } + + // Per-bundle weights + virtual double bundle_weight(const std::string& bundle_name) const = 0; + + // Per-signal weights (can override bundle weights for specific signals) + virtual double signal_weight(const std::string& bundle_name, const std::string& signal_name) const = 0; + + // Penalty and Bonus multipliers + virtual double penalty_multiplier() const { return 1.0; } + virtual double bonus_multiplier() const { return 1.0; } + + // Global scaling factor + virtual double global_scaling_factor() const { return 1.0; } + + // Falsification penalty + virtual double falsification_penalty() const { return 50.0; } // Heavy penalty for falsification +}; + +class DefaultResearchProfile : public WeightProfile { +public: + std::string name() const override { return "DefaultResearchProfile"; } + + double bundle_weight(const std::string& bundle_name) const override { + if (bundle_name == "Safety") return 1.5; + return 1.0; + } + + double signal_weight(const std::string& bundle_name, const std::string& signal_name) const override { + return 1.0; + } +}; + +class HighSafetyProfile : public WeightProfile { +public: + std::string name() const override { return "HighSafetyProfile"; } + + double bundle_weight(const std::string& bundle_name) const override { + if (bundle_name == "Safety" || bundle_name == "Cardiac" || bundle_name == "CNS" || bundle_name == "SystemicExposure") { + return 2.0; + } + return 1.0; + } + + double signal_weight(const std::string& bundle_name, const std::string& signal_name) const override { + if (signal_name.find("toxicity") != std::string::npos || signal_name.find("risk") != std::string::npos) { + return 1.5; + } + return 1.0; + } + + double penalty_multiplier() const override { return 1.5; } + double bonus_multiplier() const override { return 0.5; } // Conservative + double falsification_penalty() const override { return 100.0; } // Immediate zero or near-zero +}; + +#endif // CURAFRAME_WEIGHT_PROFILE_HPP diff --git a/scoring/WeightedScoringEngine.cpp b/scoring/WeightedScoringEngine.cpp new file mode 100644 index 0000000..fe2b89d --- /dev/null +++ b/scoring/WeightedScoringEngine.cpp @@ -0,0 +1,102 @@ +#include "WeightedScoringEngine.hpp" +#include +#include +#include +#include + +WeightedScoringEngine::WeightedScoringEngine(std::shared_ptr profile) + : profile_(std::move(profile)) {} + +ScoringReport WeightedScoringEngine::score(const EvaluationReport& eval_report) const { + ScoringReport report; + report.weight_profile_name = profile_->name(); + + double total_penalties = 0.0; + double total_bonuses = 0.0; + + // Process penalties per domain + for (const auto& domain_pair : eval_report.domain_penalty_signals) { + const std::string& domain_name = domain_pair.first; + double bundle_weight = profile_->bundle_weight(domain_name); + + double domain_penalty_sum = 0.0; + + for (const auto& signal_pair : domain_pair.second) { + const std::string& signal_name = signal_pair.first; + double raw_value = signal_pair.second; + + double signal_weight = profile_->signal_weight(domain_name, signal_name); + + if (raw_value > 0) { + // It's a penalty + double weighted_penalty = raw_value * bundle_weight * signal_weight * profile_->penalty_multiplier(); + report.penalty_breakdown[domain_name + "::" + signal_name] = weighted_penalty; + domain_penalty_sum += weighted_penalty; + } else if (raw_value < 0) { + // It's a bonus (represented as negative penalty in some contexts, or we can handle it directly) + double weighted_bonus = std::abs(raw_value) * bundle_weight * signal_weight * profile_->bonus_multiplier(); + report.bonus_breakdown[domain_name + "::" + signal_name] = weighted_bonus; + total_bonuses += weighted_bonus; + } + } + + report.bundle_contributions[domain_name] = domain_penalty_sum; + total_penalties += domain_penalty_sum; + } + + // Process falsification flags + report.falsification_flags = eval_report.falsification_flags; + if (!report.falsification_flags.empty()) { + report.falsification_impact = profile_->falsification_penalty() * report.falsification_flags.size(); + } + + // Base score is 100. We subtract penalties and falsifications, add bonuses. + double base_score = 100.0; + double raw_score = base_score - total_penalties - report.falsification_impact + total_bonuses; + + // Apply global scaling factor + raw_score *= profile_->global_scaling_factor(); + + // Clamp between 0 and 100 + report.composite_score = std::max(0.0, std::min(100.0, raw_score)); + + report.narrative_summary = generate_narrative(report, eval_report); + + return report; +} + +std::string WeightedScoringEngine::generate_narrative(const ScoringReport& report, const EvaluationReport& eval_report) const { + std::ostringstream oss; + + oss << "Scoring Summary (Profile: " << report.weight_profile_name << "):\n"; + oss << "Composite Stability Score: " << report.composite_score << "/100\n"; + + if (!report.falsification_flags.empty()) { + oss << "CRITICAL: Candidate was falsified. " << report.falsification_flags.size() << " flags raised, contributing to a massive penalty of " << report.falsification_impact << ".\n"; + } + + std::string max_penalty_domain = ""; + double max_penalty = -1.0; + for (const auto& pair : report.bundle_contributions) { + if (pair.second > max_penalty) { + max_penalty = pair.second; + max_penalty_domain = pair.first; + } + } + + if (max_penalty > 0) { + oss << "The highest risk burden originated from the " << max_penalty_domain << " bundle (" << max_penalty << " weighted penalty).\n"; + } else { + oss << "No significant domain penalties were recorded.\n"; + } + + if (!report.bonus_breakdown.empty()) { + oss << "Bonuses were applied which mitigated some risk factors.\n"; + } + + if (report.weight_profile_name == "HighSafetyProfile") { + oss << "The HighSafetyProfile heavily penalized safety and toxicity signals, resulting in a conservative stability score.\n"; + } + + return oss.str(); +} diff --git a/scoring/WeightedScoringEngine.hpp b/scoring/WeightedScoringEngine.hpp new file mode 100644 index 0000000..97aeb97 --- /dev/null +++ b/scoring/WeightedScoringEngine.hpp @@ -0,0 +1,23 @@ +#ifndef CURAFRAME_WEIGHTED_SCORING_ENGINE_HPP +#define CURAFRAME_WEIGHTED_SCORING_ENGINE_HPP + +#include "../constraint_core/EvaluationReport.hpp" +#include "ScoringReport.hpp" +#include "WeightProfile.hpp" +#include +#include + +class WeightedScoringEngine { +public: + WeightedScoringEngine(std::shared_ptr profile); + + // Ingest the evaluation report and produce a scoring report + ScoringReport score(const EvaluationReport& eval_report) const; + +private: + std::shared_ptr profile_; + + std::string generate_narrative(const ScoringReport& report, const EvaluationReport& eval_report) const; +}; + +#endif // CURAFRAME_WEIGHTED_SCORING_ENGINE_HPP diff --git a/setup.py b/setup.py index 9e4f555..a85bd75 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="CuraFrame", - version="2.5.0", + version="3.0.0", description="Constraint-driven therapeutic design reasoning framework.", packages=find_packages(exclude=("tests", "docs", "apps")), python_requires=">=3.9",