Skip to content

Commit ef4681b

Browse files
authored
Merge pull request #4 from SpatLyu/dev
enhance and wrap ccd models
2 parents d6c1c11 + 819ec8d commit ef4681b

12 files changed

Lines changed: 1081 additions & 105 deletions

File tree

DESCRIPTION

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,10 @@ BugReports: https://github.com/stscl/coupling/issues
1414
Depends:
1515
R (>= 4.1.0)
1616
LinkingTo:
17-
Rcpp
18-
Imports:
19-
methods,
20-
sdsfun,
21-
sf,
22-
terra
17+
Rcpp,
18+
RcppThread
2319
Suggests:
24-
Rcpp
20+
Rcpp,
21+
RcppThread,
2522
Config/roxygen2/markdown: TRUE
2623
Config/roxygen2/version: 8.0.0

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export(ccd)
34
useDynLib(coupling, .registration = TRUE)

R/RcppExports.R

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
2+
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393
3+
4+
RcppCCD <- function(mat, weight, method = "standard", threads = 1L) {
5+
.Call(`_coupling_RcppCCD`, mat, weight, method, threads)
6+
}
7+

R/ccd.R

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#' Coupling Coordination Degree (CCD)
2+
#'
3+
#' @param data A numeric matrix or data.frame. Rows are observations,
4+
#' columns are indicators.
5+
#' @param weight Numeric vector of indicator weights. Must have length equal
6+
#' to `ncol(data)`. If `NULL`, equal weights are used.
7+
#' @param method Coupling model. One of `"standard"`, `"wang"`, or `"fan"`.
8+
#' @param threads Number of threads used in computation.
9+
#'
10+
#' @return A data.frame with:
11+
#' \itemize{
12+
#' \item \code{C}: coupling degree
13+
#' \item \code{D}: coordination degree
14+
#' }
15+
#'
16+
#' @export
17+
#'
18+
#' @details
19+
#' Full model definitions and formulas are available at:
20+
#' \url{https://github.com/stscl/coupling/discussions/3}
21+
#'
22+
#' @note
23+
#' Input values should be normalized to `[0, 1]`.
24+
#'
25+
#' @examples
26+
#' set.seed(42)
27+
#' mat = matrix(runif(20), nrow = 5)
28+
#' coupling::ccd(mat)
29+
#'
30+
ccd = \(data, weight = NULL, method = c("standard", "wang", "fan"), threads = 1){
31+
mat = as.matrix(data)
32+
method = match.arg(method)
33+
if (is.null(weight)) weight = rep(1, times = ncol(mat)) / ncol(mat)
34+
return(RcppCCD(mat, weight, method, threads))
35+
}

inst/include/coupling.h

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,8 @@
3737
// Module Headers (Organized by functionality)
3838
// ============================================================
3939

40-
#include "coupling/embed.hpp"
41-
#include "coupling/combn.hpp"
4240
#include "coupling/numericutils.hpp"
43-
#include "coupling/distance.hpp"
44-
#include "coupling/neighbor.hpp"
45-
#include "coupling/symdync.hpp"
46-
#include "coupling/projection.hpp"
47-
#include "coupling/patcaus.hpp"
48-
#include "coupling/fnn.hpp"
41+
#include "coupling/ccd.hpp"
4942

5043
// ============================================================
5144
// Convenience Converters (Inline helpers for R/C++ interop)

inst/include/coupling/ccd.hpp

Lines changed: 46 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* ------------------------------------------------
66
*
77
* This module implements the Coupling Coordination Degree (CCD) framework,
8-
* a widely used approach for measuring the interaction, coupling strength,
8+
* a widely used method for measuring the interaction, coupling strength,
99
* and coordinated development among multiple subsystems or indicators.
1010
*
1111
* The CCD model evaluates the internal consistency of a system composed of
@@ -142,20 +142,26 @@
142142
* Notes
143143
* ---------------------------------------------------------------------------
144144
*
145-
* • All computations are performed independently for each spatial unit.
145+
* • Input values MUST be normalized to [0, 1] or be on a comparable scale.
146146
*
147-
* • Input values are assumed to be non-negative. For the standard model,
148-
* strictly positive values are required due to the geometric mean.
147+
* • If all indicators are zero (i.e., no subsystem development),
148+
* the coupling degree (C) is defined as 0.
149149
*
150-
* • Numerical safeguards may be required to avoid:
151-
* - division by zero
152-
* - negative values under square root due to floating-point errors
150+
* This avoids undefined operations (e.g., 0/0) and ensures
151+
* that systems without development are not interpreted as
152+
* having perfect coupling.
153153
*
154-
* • The Wang formulation uses pairwise absolute differences as a measure
155-
* of dispersion.
154+
* • The CCD model is scale-sensitive:
155+
* - Standard model depends on relative magnitudes
156+
* - Wang model depends on pairwise differences and max-normalization
157+
* - Fan model is variance-based
156158
*
157-
* • The ccd_c_single function is useful when computing C for a single
158-
* observation without constructing a full matrix.
159+
* • Without normalization, the coupling degree (C) may not reflect
160+
* coordination but instead reflect scale dominance.
161+
*
162+
* • Recommended preprocessing:
163+
* - Min-max normalization
164+
* - Z-score normalization (followed by rescaling to [0,1] if needed)
159165
*
160166
* ---------------------------------------------------------------------------
161167
* Author: Wenbo Lyu (Github: @SpatLyu)
@@ -171,6 +177,7 @@
171177
#include <numeric>
172178
#include <algorithm>
173179
#include <stdexcept>
180+
#include <RcppThread.h>
174181

175182
namespace coupling
176183
{
@@ -204,6 +211,10 @@ inline double ccd_c_single(
204211

205212
double geo_mean = std::pow(prod_sum, 1.0/p);
206213
double arith_mean = mean(vec);
214+
215+
if (coupling::numericutils::doubleNearlyEqual(arith_mean, 0.0)) {
216+
return 0.0;
217+
}
207218

208219
C_val = geo_mean / arith_mean;
209220
}
@@ -226,6 +237,10 @@ inline double ccd_c_single(
226237

227238
double max_u = *std::max_element(vec.begin(), vec.end());
228239

240+
if (coupling::numericutils::doubleNearlyEqual(max_u, 0.0)) {
241+
return 0.0;
242+
}
243+
229244
double prod = 1.0;
230245
for (double u : vec) {
231246
prod *= (u / max_u);
@@ -240,13 +255,18 @@ inline double ccd_c_single(
240255
// fan
241256
// =========================
242257
else if (method == "fan") {
243-
244258
double sum_u = std::accumulate(vec.begin(), vec.end(), 0.0);
259+
// if (coupling::numericutils::doubleNearlyEqual(sum_u, 0.0)) {
260+
// return 0.0;
261+
// }
245262

246263
double sum_u2 = 0.0;
247264
for (double u : vec) {
248265
sum_u2 += u * u;
249266
}
267+
// if (coupling::numericutils::doubleNearlyEqual(sum_u2, 0.0)) {
268+
// return 0.0;
269+
// }
250270

251271
double numerator = p * sum_u2 - sum_u * sum_u;
252272
double denom = p * p;
@@ -261,92 +281,27 @@ inline double ccd_c_single(
261281
throw std::invalid_argument("Unknown method");
262282
}
263283

264-
return C_val;
284+
return std::clamp(C_val, 0.0, 1.0);
265285
}
266286

267287
inline std::vector<double> ccd_c(
268288
const std::vector<std::vector<double>>& mat,
269-
const std::string& method = "standard"
289+
const std::string& method = "standard",
290+
size_t threads = 1
270291
) {
271292
size_t n_units = mat.size();
272293
if (n_units == 0) return {};
273294

274-
size_t p = mat[0].size(); // number of U values per unit
275-
276295
std::vector<double> result(n_units, 0.0);
277-
278-
for (size_t i = 0; i < n_units; ++i) {
279-
const std::vector<double>& U = mat[i];
280-
281-
// =========================
282-
// standard
283-
// =========================
284-
if (method == "standard") {
285-
double prod_sum = 1.0;
286-
for (double u : U) {
287-
// if (u <= 0) throw std::runtime_error("Values must be positive.");
288-
prod_sum *= u;
289-
}
290-
291-
double geo_mean = std::pow(prod_sum, 1.0/p);
292-
double arith_mean = mean(U);
293-
294-
result[i] = geo_mean / arith_mean;
295-
}
296-
297-
// =========================
298-
// wang
299-
// =========================
300-
else if (method == "wang") {
301-
302-
double sum_dist = 0.0;
303-
304-
for (size_t j = 0; j < p - 1; ++j) {
305-
for (size_t k = j + 1; k < p; ++k) {
306-
sum_dist += std::abs(U[j] - U[k]);
307-
}
308-
}
309-
310-
double denom = (p - 1) * p / 2.0;
311-
double term1 = 1.0 - (sum_dist / denom);
312-
if (term1 < 0) term1 = 0;
313-
314-
double max_u = *std::max_element(U.begin(), U.end());
315-
316-
double prod = 1.0;
317-
for (double u : U) {
318-
prod *= (u / max_u);
319-
}
320-
321-
double term2 = std::pow(prod, 1.0 / (p - 1));
322-
323-
result[i] = std::sqrt(term1 * term2);
324-
}
325-
326-
// =========================
327-
// fan
328-
// =========================
329-
else if (method == "fan") {
330-
331-
double sum_u = std::accumulate(U.begin(), U.end(), 0.0);
332-
333-
double sum_u2 = 0.0;
334-
for (double u : U) {
335-
sum_u2 += u * u;
336-
}
337-
338-
double numerator = p * sum_u2 - sum_u * sum_u;
339-
double denom = p * p;
340-
341-
double val = numerator / denom;
342-
if (val < 0) val = 0;
343-
344-
result[i] = 1.0 - 2.0 * std::sqrt(val);
345-
}
346-
347-
else {
348-
throw std::invalid_argument("Unknown method");
296+
297+
if (threads <= 1) {
298+
for (size_t i = 0; i < n_units; ++i) {
299+
result[i] = ccd_c_single(mat[i], method);
349300
}
301+
} else {
302+
RcppThread::parallelFor(0, n_units, [&](size_t i) {
303+
result[i] = ccd_c_single(mat[i], method);
304+
}, threads);
350305
}
351306

352307
return result;
@@ -355,14 +310,15 @@ inline std::vector<double> ccd_c(
355310
inline std::vector<std::vector<double>> ccd(
356311
const std::vector<std::vector<double>>& mat,
357312
const std::vector<double>& weight,
358-
const std::string& method = "standard"
313+
const std::string& method = "standard",
314+
size_t threads = 1
359315
) {
360316
size_t n_units = mat.size(); // number of unit
361317
size_t p = mat[0].size(); // number of U values per unit
362318

363319
std::vector<std::vector<double>> result(2, std::vector<double>(n_units, 0.0));
364320

365-
std::vector<double> C_vals = ccd_c(mat, method); // C values
321+
std::vector<double> C_vals = ccd_c(mat, method, threads); // C values
366322
result[0] = C_vals;
367323

368324
for (size_t i = 0; i < n_units; ++i) {

0 commit comments

Comments
 (0)