1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
|
#define NPY_NO_DEPRECATED_API NPY_API_VERSION
#include <stdbool.h>
#include <Python.h>
#include <structmember.h>
#include <numpy/arrayobject.h>
#include <signal.h>
// Python is silly. There's some nuance about signal handling where it sets a
// SIGINT (ctrl-c) handler to just set a flag, and the python layer then reads
// this flag and does the thing. Here I'm running C code, so SIGINT would set a
// flag, but not quit, so I can't interrupt the solver. Thus I reset the SIGINT
// handler to the default, and put it back to the python-specific version when
// I'm done
#define SET_SIGINT() struct sigaction sigaction_old; \
do { \
if( 0 != sigaction(SIGINT, \
&(struct sigaction){ .sa_handler = SIG_DFL }, \
&sigaction_old) ) \
{ \
PyErr_SetString(PyExc_RuntimeError, "sigaction() failed"); \
goto done; \
} \
} while(0)
#define RESET_SIGINT() do { \
if( 0 != sigaction(SIGINT, \
&sigaction_old, NULL )) \
PyErr_SetString(PyExc_RuntimeError, "sigaction-restore failed"); \
} while(0)
static
bool parse_dim_for_one_arg(// input and output
// so-far-seen named dimensions. Initially these are
// <0 to indicate that they're unknown. As the
// broadcasting rules determine the values of these,
// the values are stored here (>= 0), and checked for
// consistency
npy_intp* dims_named,
// so-far-seen broadcasted dimensions. Initially
// these are 1 to indicate that these are compatible
// with anything. As non-1 values are seen, those are
// stored here (> 1), and checked for consistency
npy_intp* dims_extra,
// input
const int Ndims_extra,
const int Ndims_extra_inputs_only,
const char* arg_name,
const int Ndims_extra_var,
const npy_intp* dims_want, const int Ndims_want,
const npy_intp* dims_var, const int Ndims_var,
const bool is_output)
{
// MAKE SURE THE PROTOTYPE DIMENSIONS MATCH (the trailing dimensions)
//
// Loop through the dimensions. Set the dimensionality of any new named
// argument to whatever the current argument has. Any already-known
// argument must match
for( int i_dim=-1;
i_dim >= -Ndims_want;
i_dim--)
{
int i_dim_want = i_dim + Ndims_want;
int dim_want = dims_want[i_dim_want];
int i_dim_var = i_dim + Ndims_var;
// if we didn't get enough dimensions, use dim=1
int dim_var = i_dim_var >= 0 ? dims_var[i_dim_var] : 1;
if(dim_want < 0)
{
// This is a named dimension. These can have any value, but
// ALL dimensions of the same name must thave the SAME value
// EVERYWHERE
if(dims_named[-dim_want-1] < 0)
dims_named[-dim_want-1] = dim_var;
dim_want = dims_named[-dim_want-1];
}
// The prototype dimension (named or otherwise) now has a numeric
// value. Make sure it matches what I have
if(dim_want != dim_var)
{
if(dims_want[i_dim_want] < 0)
PyErr_Format(PyExc_RuntimeError,
"Argument '%s': prototype says dimension %d (named dimension %d) has length %d, but got %d",
arg_name,
i_dim, (int)dims_want[i_dim_want],
dim_want,
dim_var);
else
PyErr_Format(PyExc_RuntimeError,
"Argument '%s': prototype says dimension %d has length %d, but got %d",
arg_name,
i_dim,
dim_want,
dim_var);
return false;
}
}
// I now know that this argument matches the prototype. I look at the
// extra dimensions to broadcast, and make sure they match with the
// dimensions I saw previously
// MAKE SURE THE BROADCASTED DIMENSIONS MATCH (the leading dimensions)
//
// This argument has Ndims_extra_var dimensions above the prototype (may be
// <0 if there're implicit leading length-1 dimensions at the start). The
// current dimensions to broadcast must match
// outputs may be bigger than the inputs (this will result in multiple
// identical copies in each slice), but may not be smaller. I check that
// existing extra dimensions are sufficiently large. And then I check to
// make sure we have enough extra dimensions
if(is_output)
{
for( int i_dim=-1;
i_dim >= -Ndims_extra_var;
i_dim--)
{
const int i_dim_var = i_dim - Ndims_want + Ndims_var;
// if we didn't get enough dimensions, use dim=1
const int dim_var = i_dim_var >= 0 ? dims_var[i_dim_var] : 1;
const int i_dim_extra = i_dim + Ndims_extra;
if(dim_var < dims_extra[i_dim_extra])
{
PyErr_Format(PyExc_RuntimeError,
"Output '%s' dimension %d (broadcasted dimension %d) too small. Inputs have length %d but this output has length %d",
arg_name,
i_dim-Ndims_want, i_dim,
(int)dims_extra[i_dim_extra],
dim_var);
return false;
}
}
// I look through extra dimensions above what this output has to make
// sure that the output array is big-enough to hold all the output. I
// only care about the broadcasted slices defined by the input. Because
// I don't NEED to store all the duplicates created by the output-only
// broadcasting
for( int i_dim=-Ndims_extra_var-1;
i_dim >= -Ndims_extra_inputs_only;
i_dim--)
{
const int i_dim_extra = i_dim + Ndims_extra;
// What if this test passes, but a subsequent output increases
// dims_extra[i_dim_extra] so that this would have failed? That is
// OK. Extra dimensions in the outputs do not create new and
// different results, and I don't need to make sure I have room to
// store duplicates
if(dims_extra[i_dim_extra] > 1)
{
// This dimension was set, but this array has a DIFFERENT value
PyErr_Format(PyExc_RuntimeError,
"Argument '%s' dimension %d (broadcasted dimension %d) is too small: this dimension of this output is too small to hold the broadcasted results of size %d",
arg_name,
i_dim-Ndims_want, i_dim,
(int)dims_extra[i_dim_extra]);
return false;
}
}
}
for( int i_dim=-1;
i_dim >= -Ndims_extra_var;
i_dim--)
{
const int i_dim_var = i_dim - Ndims_want + Ndims_var;
// if we didn't get enough dimensions, use dim=1
const int dim_var = i_dim_var >= 0 ? dims_var[i_dim_var] : 1;
const int i_dim_extra = i_dim + Ndims_extra;
if (dim_var != 1)
{
if(i_dim_extra < 0)
{
PyErr_Format(PyExc_RuntimeError,
"Argument '%s' dimension %d (broadcasted dimension %d) i_dim_extra<0: %d. This shouldn't happen. There's a bug in the implicit-leading-dimension logic. Please report",
arg_name,
i_dim-Ndims_want, i_dim,
i_dim_extra);
return false;
}
// I have a new value for this dimension
if( dims_extra[i_dim_extra] == 1)
// This dimension wasn't set yet; I set it
dims_extra[i_dim_extra] = dim_var;
else if(dims_extra[i_dim_extra] != dim_var)
{
// This dimension was set, but this array has a DIFFERENT value
PyErr_Format(PyExc_RuntimeError,
"Argument '%s' dimension %d (broadcasted dimension %d) mismatch. Previously saw length %d, but here have length %d",
arg_name,
i_dim-Ndims_want, i_dim,
(int)dims_extra[i_dim_extra],
dim_var);
return false;
}
}
}
return true;
}
|