extern crate nalgebra as na;
use crate::{
resource::{ProblemResourceImpl, ResourceLocation, StorageConfig},
utils::LassoError,
};
use memmap2::Mmap;
use rayon::prelude::*;
use std::{fs::File, io::BufWriter, io::Write, path::Path};
#[derive(Clone)]
pub struct MatrixVariable {
id: String,
_resource: ProblemResourceImpl<na::DMatrix<f32>>,
}
impl MatrixVariable {
pub fn new(
id: String,
default_location: ResourceLocation,
storage_config: &StorageConfig,
) -> Self {
MatrixVariable {
id: id.clone(),
_resource: ProblemResourceImpl::new(id, default_location, storage_config),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub async fn from_matrix(
id: String,
default_location: ResourceLocation,
storage_config: &StorageConfig,
matrix: na::DMatrix<f32>,
) -> Self {
let mut variable = MatrixVariable::new(id, default_location, storage_config);
let _ = variable._resource.write(matrix).await.unwrap();
variable
}
pub async fn zeros(
id: String,
nrows: usize,
ncols: usize,
default_location: ResourceLocation,
storage_config: &StorageConfig,
) -> Self {
let matrix = na::DMatrix::<f32>::zeros(nrows, ncols);
MatrixVariable::from_matrix(id, default_location, storage_config, matrix).await
}
pub async fn ones(
id: String,
nrows: usize,
ncols: usize,
default_location: ResourceLocation,
storage_config: &StorageConfig,
) -> Self {
let matrix = na::DMatrix::<f32>::from_element(nrows, ncols, 1.0);
MatrixVariable::from_matrix(id, default_location, storage_config, matrix).await
}
pub async fn eye(
id: String,
nrows: usize,
ncols: usize,
default_location: ResourceLocation,
storage_config: &StorageConfig,
) -> Self {
let matrix = na::DMatrix::identity(nrows, ncols);
MatrixVariable::from_matrix(id, default_location, storage_config, matrix).await
}
pub async fn sync(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self._resource.sync().await
}
pub async fn sync_to(
&mut self,
location: ResourceLocation,
) -> Result<(), Box<dyn std::error::Error>> {
self._resource.sync_to(location).await
}
pub fn default_location(&self) -> ResourceLocation {
self._resource.default_location()
}
pub async fn read(&self) -> Result<na::DMatrix<f32>, Box<dyn std::error::Error>> {
self._resource.read().await
}
pub async fn read_and_cache(
&mut self,
location: ResourceLocation,
) -> Result<na::DMatrix<f32>, Box<dyn std::error::Error>> {
self._resource.read_and_cache(location).await
}
pub async fn write(
&mut self,
matrix: na::DMatrix<f32>,
) -> Result<(), Box<dyn std::error::Error>> {
self._resource.write(matrix).await
}
pub fn s3_key(&self) -> String {
self._resource.s3_key()
}
pub fn s3_bucket(&self) -> String {
self._resource.s3_bucket()
}
pub fn set_default_location(&mut self, location: ResourceLocation) {
self._resource.set_default_location(location);
}
pub fn mark_updated(&mut self, location: ResourceLocation) {
self._resource.mark_updated(location);
}
pub async fn sub(
&mut self,
other: &mut MatrixVariable,
) -> Result<MatrixVariable, Box<dyn std::error::Error>> {
let target_location = self.default_location();
self._resource.sync_to(target_location).await?;
other._resource.sync_to(target_location).await?;
let self_matrix = self._resource.read().await?;
let other_matrix = other._resource.read().await?;
let result = self_matrix - other_matrix;
Ok(MatrixVariable::from_matrix(
format!("{}-sub-{}", self.id, other.id),
target_location,
self._resource.storage_config(),
result,
)
.await)
}
pub async fn add(
&mut self,
other: &mut MatrixVariable,
) -> Result<(), Box<dyn std::error::Error>> {
let target_location = self.default_location();
self._resource.sync_to(target_location).await?;
other._resource.sync_to(target_location).await?;
let self_matrix = self._resource.read().await?;
let other_matrix = other._resource.read().await?;
let result = self_matrix + other_matrix;
self._resource.write(result).await?;
Ok(())
}
pub async fn add_identity(&mut self, factor: f32) -> Result<(), Box<dyn std::error::Error>> {
self._resource.sync().await?;
let matrix = self._resource.read().await?;
let nrows = matrix.nrows();
let ncols = matrix.ncols();
let eye = na::DMatrix::<f32>::identity(nrows, ncols);
let result = matrix + eye * factor;
self._resource.write(result).await?;
Ok(())
}
pub async fn inverse(&mut self) -> Result<MatrixVariable, Box<dyn std::error::Error>> {
self._resource.sync().await?;
let matrix = self._resource.read().await?;
let inverse = matrix.try_inverse().ok_or("Matrix is not invertible")?;
Ok(MatrixVariable::from_matrix(
format!("{}-inverse", self.id),
self.default_location(),
self._resource.storage_config(),
inverse,
)
.await)
}
pub async fn map_rows_by_subproblem<R, F>(
&self,
n_subproblems: usize,
mut f: F,
) -> Result<Vec<R>, Box<dyn std::error::Error>>
where
F: FnMut(na::DMatrix<f32>, usize) -> R,
{
let matrix = self._resource.read().await?;
let nrows = matrix.nrows();
let mut results = Vec::new();
let base_chunk_size = nrows / n_subproblems;
let remainder = nrows % n_subproblems;
let mut current_row = 0;
for i in 0..n_subproblems {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk = matrix.rows(current_row, chunk_size).clone_owned();
results.push(f(chunk, i));
current_row += chunk_size;
}
Ok(results)
}
pub async fn map_rows_by_subproblem_with<R, F>(
&self,
var2: &MatrixVariable,
n_subproblems: usize,
mut f: F,
) -> Result<Vec<R>, Box<dyn std::error::Error>>
where
F: FnMut(na::DMatrix<f32>, na::DMatrix<f32>, usize) -> R,
{
let matrix = self._resource.read().await?;
let matrix2 = var2._resource.read().await?;
let nrows = matrix.nrows();
let nrows2 = matrix2.nrows();
let ncols = matrix.ncols();
let ncols2 = matrix2.ncols();
let mut results = Vec::new();
if nrows != nrows2 {
return Err(LassoError::from_string(format!(
"Matrix rows do not match: {}x{} and {}x{}",
nrows, ncols, nrows2, ncols2
))
.into());
}
let base_chunk_size = nrows / n_subproblems;
let remainder = nrows % n_subproblems;
let mut current_row = 0;
for i in 0..n_subproblems {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk = matrix.rows(current_row, chunk_size).clone_owned();
let chunk2 = matrix2.rows(current_row, chunk_size).clone_owned();
results.push(f(chunk, chunk2, i));
current_row += chunk_size;
}
Ok(results)
}
pub async fn map_rows_by_subproblem_with_rayon<R, F>(
&self,
var2: &MatrixVariable,
n_subproblems: usize,
n_threads: usize,
f: F,
) -> Result<Vec<R>, Box<dyn std::error::Error>>
where
F: Fn(na::DMatrix<f32>, na::DMatrix<f32>, usize) -> R + Send + Sync,
R: Send,
{
let matrix = self._resource.read().await?;
let matrix2 = var2._resource.read().await?;
let nrows = matrix.nrows();
let nrows2 = matrix2.nrows();
let ncols = matrix.ncols();
let ncols2 = matrix2.ncols();
if nrows != nrows2 {
return Err(LassoError::from_string(format!(
"Matrix rows do not match: {}x{} and {}x{}",
nrows, ncols, nrows2, ncols2
))
.into());
}
let base_chunk_size = nrows / n_subproblems;
let remainder = nrows % n_subproblems;
let chunk_data = (0..n_subproblems)
.map(|i| {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk_index = base_chunk_size * i + if i < remainder { i } else { remainder };
(chunk_index, chunk_size)
})
.collect::<Vec<_>>();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(n_threads)
.build()
.unwrap();
let results = pool.install(|| {
chunk_data
.into_par_iter()
.zip(0..n_subproblems)
.map(|((chunk_index, chunk_size), i)| {
let chunk = matrix.rows(chunk_index, chunk_size).clone_owned();
let chunk2 = matrix2.rows(chunk_index, chunk_size).clone_owned();
f(chunk, chunk2, i)
})
.collect::<Vec<_>>()
});
Ok(results)
}
pub async fn map_rows_by_subproblem_with_async<R, F>(
&self,
var2: &MatrixVariable,
n_subproblems: usize,
f: F,
) -> Result<Vec<R>, Box<dyn std::error::Error>>
where
F: AsyncFn(na::DMatrix<f32>, na::DMatrix<f32>, usize) -> R,
{
let matrix = self._resource.read().await?;
let matrix2 = var2._resource.read().await?;
let nrows = matrix.nrows();
let nrows2 = matrix2.nrows();
let ncols = matrix.ncols();
let ncols2 = matrix2.ncols();
if nrows != nrows2 {
return Err(LassoError::from_string(format!(
"Matrix rows do not match: {}x{} and {}x{}",
nrows, ncols, nrows2, ncols2
))
.into());
}
let base_chunk_size = nrows / n_subproblems;
let remainder = nrows % n_subproblems;
let mut current_row = 0;
let futures = (0..n_subproblems)
.map(|i| {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk = matrix.rows(current_row, chunk_size).clone_owned();
let chunk2 = matrix2.rows(current_row, chunk_size).clone_owned();
current_row += chunk_size;
f(chunk, chunk2, i)
})
.collect::<Vec<_>>();
let results = futures::future::join_all(futures).await;
Ok(results)
}
}
pub struct ScalarVariable {
#[allow(dead_code)]
id: String,
_resource: ProblemResourceImpl<f32>,
}
impl ScalarVariable {
pub fn new(
id: String,
default_location: ResourceLocation,
storage_config: &StorageConfig,
) -> Self {
ScalarVariable {
id: id.clone(),
_resource: ProblemResourceImpl::new(id, default_location, storage_config),
}
}
pub async fn from_scalar(
id: String,
default_location: ResourceLocation,
storage_config: &StorageConfig,
scalar: f32,
) -> Self {
let mut variable = ScalarVariable::new(id, default_location, storage_config);
let _ = variable._resource.write(scalar).await.unwrap();
variable
}
pub async fn read(&self) -> Result<f32, Box<dyn std::error::Error>> {
self._resource.read().await
}
pub async fn read_and_cache(
&mut self,
location: ResourceLocation,
) -> Result<f32, Box<dyn std::error::Error>> {
self._resource.read_and_cache(location).await
}
pub async fn write(&mut self, scalar: f32) -> Result<(), Box<dyn std::error::Error>> {
self._resource.write(scalar).await
}
pub fn s3_key(&self) -> String {
self._resource.s3_key()
}
pub fn s3_bucket(&self) -> String {
self._resource.s3_bucket()
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum MatrixStorageType {
Columns = 0,
Rows = 1,
}
impl std::fmt::Display for MatrixStorageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatrixStorageType::Columns => write!(f, "Columns"),
MatrixStorageType::Rows => write!(f, "Rows"),
}
}
}
pub struct DataMatrixVariable {
id: String,
_resource: ProblemResourceImpl<na::DMatrix<f32>>,
storage_type: MatrixStorageType,
nrows: usize,
ncols: usize,
}
impl DataMatrixVariable {
pub(crate) fn new(
id: String,
storage_config: &StorageConfig,
storage_type: MatrixStorageType,
nrows: usize,
ncols: usize,
) -> Self {
DataMatrixVariable {
id: id.clone(),
_resource: ProblemResourceImpl::new(id, ResourceLocation::Local, storage_config),
storage_type,
nrows,
ncols,
}
}
pub fn from_matrix(
id: String,
matrix: na::DMatrix<f32>,
storage_type: MatrixStorageType,
storage_config: &StorageConfig,
) -> Self {
let var = DataMatrixVariable::new(
id,
storage_config,
storage_type,
matrix.nrows(),
matrix.ncols(),
);
let path = var._resource.local_path();
write_data_matrix_to_file(&matrix, &path, storage_type).unwrap();
var
}
pub fn from_problem_file(id: String, file_path: &Path, storage_config: &StorageConfig) -> Self {
let new_path = Path::new(&storage_config.local.root)
.join(format!("{}{}", storage_config.local.prefix, id))
.with_extension("bin");
std::fs::copy(file_path, &new_path).unwrap();
let file = File::open(new_path).unwrap();
let mmap = unsafe { Mmap::map(&file).unwrap() };
let header = mmap[0..24].to_vec();
let nrows = u64::from_le_bytes(header[8..16].try_into().unwrap()) as usize;
let ncols = u64::from_le_bytes(header[16..24].try_into().unwrap()) as usize;
let storage_type = match header[0] {
0 => MatrixStorageType::Columns,
1 => MatrixStorageType::Rows,
_ => panic!("Invalid storage type"),
};
let var = DataMatrixVariable::new(id, storage_config, storage_type, nrows, ncols);
var
}
pub fn read_chunk(
&self,
chunk_index: usize,
chunk_size: usize,
) -> Result<na::DMatrix<f32>, Box<dyn std::error::Error>> {
let mut path = self._resource.local_path();
path.set_extension(""); let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
let header = mmap[0..24].to_vec();
let nrows = u64::from_le_bytes(header[8..16].try_into().unwrap()) as usize;
let ncols = u64::from_le_bytes(header[16..24].try_into().unwrap()) as usize;
return match self.storage_type {
MatrixStorageType::Columns => {
if chunk_index + chunk_size > ncols {
return Err(LassoError::from_string(format!(
"Chunk index and size exceed number of columns: {} + {} > {}",
chunk_index, chunk_size, ncols
))
.into());
}
let start_index = 24 + chunk_index * 4 * nrows;
let end_index = start_index + chunk_size * 4 * nrows;
let data = &mmap[start_index..end_index];
let data_f32: &[f32] = unsafe {
std::slice::from_raw_parts(data.as_ptr() as *const f32, nrows * chunk_size)
};
let matrix = na::DMatrix::<f32>::from_column_slice(nrows, chunk_size, data_f32);
Ok(matrix)
}
MatrixStorageType::Rows => {
if chunk_index + chunk_size > nrows {
return Err(LassoError::from_string(format!(
"Chunk index and size exceed number of rows: {} + {} > {}",
chunk_index, chunk_size, nrows
))
.into());
}
let start_index = 24 + chunk_index * ncols * 4;
let end_index = start_index + chunk_size * ncols * 4;
let data = &mmap[start_index..end_index];
let data_f32: &[f32] = unsafe {
std::slice::from_raw_parts(data.as_ptr() as *const f32, chunk_size * ncols)
};
let matrix = na::DMatrix::<f32>::from_row_slice(chunk_size, ncols, data_f32);
Ok(matrix)
}
};
}
pub fn iter_subproblems<T, F>(
&self,
n_subproblems: usize,
f: F,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
F: Fn(na::DMatrix<f32>, usize) -> T,
{
let base_chunk_size = self.nrows() / n_subproblems;
let remainder = self.nrows() % n_subproblems;
let chunk_data = (0..n_subproblems)
.map(|i| {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk_index = base_chunk_size * i + if i < remainder { i } else { remainder };
(chunk_index, chunk_size)
})
.collect::<Vec<_>>();
let results = chunk_data
.into_iter()
.enumerate()
.map(|(i, (chunk_index, chunk_size))| {
let chunk = self
.read_chunk(chunk_index, chunk_size)
.expect("Failed to read chunk");
f(chunk, i)
})
.collect::<Vec<_>>();
Ok(results)
}
pub fn iter_subproblems_with<T, F>(
&self,
var2: &DataMatrixVariable,
n_subproblems: usize,
f: F,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
F: Fn(na::DMatrix<f32>, na::DMatrix<f32>, usize) -> T,
{
let base_chunk_size = self.nrows() / n_subproblems;
let remainder = self.nrows() % n_subproblems;
if self.storage_type != var2.storage_type {
return Err(LassoError::from_string(format!(
"Storage types do not match: {} and {}",
self.storage_type, var2.storage_type
))
.into());
}
match self.storage_type {
MatrixStorageType::Columns => {
if self.ncols() != var2.ncols() {
return Err(LassoError::from_string(format!(
"Number of columns do not match: {} and {}",
self.ncols(),
var2.ncols()
))
.into());
}
}
MatrixStorageType::Rows => {
if self.nrows() != var2.nrows() {
return Err(LassoError::from_string(format!(
"Number of rows do not match: {} and {}",
self.nrows(),
var2.nrows()
))
.into());
}
}
}
let chunk_data = (0..n_subproblems)
.map(|i| {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk_index = base_chunk_size * i + if i < remainder { i } else { remainder };
(chunk_index, chunk_size)
})
.collect::<Vec<_>>();
let results = chunk_data
.into_iter()
.enumerate()
.map(|(i, (chunk_index, chunk_size))| {
let chunk = self
.read_chunk(chunk_index, chunk_size)
.expect("Failed to read chunk");
let chunk2 = var2
.read_chunk(chunk_index, chunk_size)
.expect("Failed to read chunk");
f(chunk, chunk2, i)
})
.collect::<Vec<_>>();
Ok(results)
}
pub fn iter_subproblems_rayon<T, F>(
&self,
n_subproblems: usize,
n_threads: usize,
f: F,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
F: Fn(na::DMatrix<f32>, usize) -> T + Send + Sync,
T: Send,
{
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(n_threads)
.build()
.unwrap();
let base_chunk_size = self.nrows() / n_subproblems;
let remainder = self.nrows() % n_subproblems;
let chunk_data = (0..n_subproblems)
.map(|i| {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk_index = base_chunk_size * i + if i < remainder { i } else { remainder };
(chunk_index, chunk_size)
})
.collect::<Vec<_>>();
let results = pool.install(|| {
chunk_data
.into_par_iter()
.zip(0..n_subproblems)
.map(|((chunk_index, chunk_size), i)| {
let chunk = self
.read_chunk(chunk_index, chunk_size)
.expect("Failed to read chunk");
f(chunk, i)
})
.collect::<Vec<_>>()
});
Ok(results)
}
pub fn iter_subproblems_with_rayon<T, F>(
&self,
var2: &DataMatrixVariable,
n_subproblems: usize,
n_threads: usize,
f: F,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
F: Fn(na::DMatrix<f32>, na::DMatrix<f32>, usize) -> T + Send + Sync,
T: Send,
{
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(n_threads)
.build()
.unwrap();
let base_chunk_size = self.nrows() / n_subproblems;
let remainder = self.nrows() % n_subproblems;
if self.storage_type != var2.storage_type {
return Err(LassoError::from_string(format!(
"Storage types do not match: {} and {}",
self.storage_type, var2.storage_type
))
.into());
}
match self.storage_type {
MatrixStorageType::Columns => {
if self.ncols() != var2.ncols() {
return Err(LassoError::from_string(format!(
"Number of columns do not match: {} and {}",
self.ncols(),
var2.ncols()
))
.into());
}
}
MatrixStorageType::Rows => {
if self.nrows() != var2.nrows() {
return Err(LassoError::from_string(format!(
"Number of rows do not match: {} and {}",
self.nrows(),
var2.nrows()
))
.into());
}
}
}
let chunk_data = (0..n_subproblems)
.map(|i| {
let chunk_size = if i < remainder {
base_chunk_size + 1
} else {
base_chunk_size
};
let chunk_index = base_chunk_size * i + if i < remainder { i } else { remainder };
(chunk_index, chunk_size)
})
.collect::<Vec<_>>();
let results = pool.install(|| {
chunk_data
.into_par_iter()
.zip(0..n_subproblems)
.map(|((chunk_index, chunk_size), i)| {
let chunk = self
.read_chunk(chunk_index, chunk_size)
.expect("Failed to read chunk");
let chunk2 = var2
.read_chunk(chunk_index, chunk_size)
.expect("Failed to read chunk");
f(chunk, chunk2, i)
})
.collect::<Vec<_>>()
});
Ok(results)
}
pub fn id(&self) -> &str {
&self.id
}
pub fn nrows(&self) -> usize {
self.nrows
}
pub fn ncols(&self) -> usize {
self.ncols
}
}
pub(crate) fn write_data_matrix_to_file(
matrix: &na::DMatrix<f32>,
file_path: &Path,
storage_type: MatrixStorageType,
) -> Result<(), Box<dyn std::error::Error>> {
let batch_size = 1024;
let file = File::create(file_path)?;
let nrows = matrix.nrows() as u64;
let ncols = matrix.ncols() as u64;
let buf_size = match storage_type {
MatrixStorageType::Columns => nrows * 4,
MatrixStorageType::Rows => ncols * 4,
};
let mut writer = BufWriter::with_capacity(buf_size as usize, file);
let header = u64_to_bytes(storage_type as u64);
writer.write_all(&header)?;
let header = [nrows, ncols];
let header = header
.iter()
.map(|&v| u64_to_bytes(v))
.flatten()
.collect::<Vec<_>>();
writer.write_all(&header)?;
let batches = match storage_type {
MatrixStorageType::Columns => (0..matrix.ncols()).step_by(batch_size),
MatrixStorageType::Rows => (0..matrix.nrows()).step_by(batch_size),
};
match storage_type {
MatrixStorageType::Columns => {
for col in batches {
let start_col = col;
let n_cols = std::cmp::min(start_col + batch_size, matrix.ncols()) - start_col;
let col_view = matrix.columns(start_col, n_cols).clone_owned();
let col_data = col_view.as_slice();
let byte_slice = unsafe {
std::slice::from_raw_parts(col_data.as_ptr() as *const u8, col_data.len() * 4)
};
writer.write_all(byte_slice)?;
}
}
MatrixStorageType::Rows => {
for row in batches {
let start_row = row;
let n_rows = std::cmp::min(start_row + batch_size, matrix.nrows()) - start_row;
let row_data_t = matrix.rows(start_row, n_rows).clone_owned().transpose();
let row_data = row_data_t.as_slice();
let byte_slice = unsafe {
std::slice::from_raw_parts(row_data.as_ptr() as *const u8, row_data.len() * 4)
};
writer.write_all(byte_slice)?;
}
}
}
Ok(())
}
pub(crate) fn u64_to_bytes(value: u64) -> [u8; 8] {
let mut bytes = [0; 8];
bytes.copy_from_slice(&value.to_le_bytes());
bytes
}